Merge "Add AggregateAppFunctionInvoker" into androidx-main
diff --git a/appfunctions/appfunctions-common/api/current.txt b/appfunctions/appfunctions-common/api/current.txt
index 16cb63b..ba35715 100644
--- a/appfunctions/appfunctions-common/api/current.txt
+++ b/appfunctions/appfunctions-common/api/current.txt
@@ -1,11 +1,14 @@
 // Signature format: 4.0
 package androidx.appfunctions {
 
-  public final class AppFunctionAppUnknownException extends androidx.appfunctions.AppFunctionException {
+  public abstract class AppFunctionAppException extends androidx.appfunctions.AppFunctionException {
+  }
+
+  public final class AppFunctionAppUnknownException extends androidx.appfunctions.AppFunctionAppException {
     ctor public AppFunctionAppUnknownException(optional String? errorMessage);
   }
 
-  public final class AppFunctionCancelledException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionCancelledException extends androidx.appfunctions.AppFunctionSystemException {
     ctor public AppFunctionCancelledException(optional String? errorMessage);
   }
 
@@ -18,93 +21,69 @@
     property public abstract android.content.Context context;
   }
 
-  public final class AppFunctionDeniedException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionDeniedException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionDeniedException(optional String? errorMessage);
   }
 
-  public final class AppFunctionDisabledException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionDisabledException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionDisabledException(optional String? errorMessage);
   }
 
-  public final class AppFunctionElementAlreadyExistsException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionElementAlreadyExistsException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionElementAlreadyExistsException(optional String? errorMessage);
   }
 
-  public final class AppFunctionElementNotFoundException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionElementNotFoundException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionElementNotFoundException(optional String? errorMessage);
   }
 
-  public class AppFunctionException extends java.lang.Exception {
+  public abstract class AppFunctionException extends java.lang.Exception {
     ctor public AppFunctionException(int errorCode, optional String? errorMessage);
-    method public final int getErrorCategory();
-    method public final int getErrorCode();
     method public final String? getErrorMessage();
-    property public final int errorCategory;
-    property public final int errorCode;
     property public final String? errorMessage;
     field public static final androidx.appfunctions.AppFunctionException.Companion Companion;
-    field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8
-    field public static final int ERROR_CANCELLED = 2001; // 0x7d1
-    field public static final int ERROR_CATEGORY_APP = 3; // 0x3
-    field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1
-    field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2
-    field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0
-    field public static final int ERROR_DENIED = 1000; // 0x3e8
-    field public static final int ERROR_DISABLED = 1002; // 0x3ea
-    field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb
-    field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9
-    field public static final int ERROR_LIMIT_EXCEEDED = 1501; // 0x5dd
-    field public static final int ERROR_NOT_SUPPORTED = 3501; // 0xdad
-    field public static final int ERROR_PERMISSION_REQUIRED = 3500; // 0xdac
-    field public static final int ERROR_RESOURCE_ALREADY_EXISTS = 1502; // 0x5de
-    field public static final int ERROR_RESOURCE_NOT_FOUND = 1500; // 0x5dc
-    field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0
   }
 
   public static final class AppFunctionException.Companion {
-    property public static final int ERROR_APP_UNKNOWN_ERROR;
-    property public static final int ERROR_CANCELLED;
-    property public static final int ERROR_CATEGORY_APP;
-    property public static final int ERROR_CATEGORY_REQUEST_ERROR;
-    property public static final int ERROR_CATEGORY_SYSTEM;
-    property public static final int ERROR_CATEGORY_UNKNOWN;
-    property public static final int ERROR_DENIED;
-    property public static final int ERROR_DISABLED;
-    property public static final int ERROR_FUNCTION_NOT_FOUND;
-    property public static final int ERROR_INVALID_ARGUMENT;
-    property public static final int ERROR_LIMIT_EXCEEDED;
-    property public static final int ERROR_NOT_SUPPORTED;
-    property public static final int ERROR_PERMISSION_REQUIRED;
-    property public static final int ERROR_RESOURCE_ALREADY_EXISTS;
-    property public static final int ERROR_RESOURCE_NOT_FOUND;
-    property public static final int ERROR_SYSTEM_ERROR;
   }
 
-  public final class AppFunctionFunctionNotFoundException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionFunctionNotFoundException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionFunctionNotFoundException(optional String? errorMessage);
   }
 
-  public final class AppFunctionInvalidArgumentException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionInvalidArgumentException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionInvalidArgumentException(optional String? errorMessage);
   }
 
-  public final class AppFunctionLimitExceededException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionLimitExceededException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionLimitExceededException(optional String? errorMessage);
   }
 
-  public final class AppFunctionNotSupportedException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionNotSupportedException extends androidx.appfunctions.AppFunctionAppException {
     ctor public AppFunctionNotSupportedException(optional String? errorMessage);
   }
 
-  public final class AppFunctionPermissionRequiredException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionPermissionRequiredException extends androidx.appfunctions.AppFunctionAppException {
     ctor public AppFunctionPermissionRequiredException(optional String? errorMessage);
   }
 
+  public abstract class AppFunctionRequestException extends androidx.appfunctions.AppFunctionException {
+  }
+
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface AppFunctionSerializable {
   }
 
-  public final class AppFunctionSystemException extends androidx.appfunctions.AppFunctionException {
-    ctor public AppFunctionSystemException(optional String? errorMessage);
+  public abstract class AppFunctionSystemException extends androidx.appfunctions.AppFunctionException {
+  }
+
+  public final class AppFunctionSystemUnknownException extends androidx.appfunctions.AppFunctionSystemException {
+    ctor public AppFunctionSystemUnknownException(optional String? errorMessage);
+  }
+
+  public final class AppFunctionUnknownException extends androidx.appfunctions.AppFunctionException {
+    ctor public AppFunctionUnknownException(int unknownErrorCode, optional String? errorMessage);
+    method public int getUnknownErrorCode();
+    property public final int unknownErrorCode;
   }
 
 }
diff --git a/appfunctions/appfunctions-common/api/restricted_current.txt b/appfunctions/appfunctions-common/api/restricted_current.txt
index 16cb63b..ba35715 100644
--- a/appfunctions/appfunctions-common/api/restricted_current.txt
+++ b/appfunctions/appfunctions-common/api/restricted_current.txt
@@ -1,11 +1,14 @@
 // Signature format: 4.0
 package androidx.appfunctions {
 
-  public final class AppFunctionAppUnknownException extends androidx.appfunctions.AppFunctionException {
+  public abstract class AppFunctionAppException extends androidx.appfunctions.AppFunctionException {
+  }
+
+  public final class AppFunctionAppUnknownException extends androidx.appfunctions.AppFunctionAppException {
     ctor public AppFunctionAppUnknownException(optional String? errorMessage);
   }
 
-  public final class AppFunctionCancelledException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionCancelledException extends androidx.appfunctions.AppFunctionSystemException {
     ctor public AppFunctionCancelledException(optional String? errorMessage);
   }
 
@@ -18,93 +21,69 @@
     property public abstract android.content.Context context;
   }
 
-  public final class AppFunctionDeniedException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionDeniedException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionDeniedException(optional String? errorMessage);
   }
 
-  public final class AppFunctionDisabledException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionDisabledException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionDisabledException(optional String? errorMessage);
   }
 
-  public final class AppFunctionElementAlreadyExistsException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionElementAlreadyExistsException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionElementAlreadyExistsException(optional String? errorMessage);
   }
 
-  public final class AppFunctionElementNotFoundException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionElementNotFoundException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionElementNotFoundException(optional String? errorMessage);
   }
 
-  public class AppFunctionException extends java.lang.Exception {
+  public abstract class AppFunctionException extends java.lang.Exception {
     ctor public AppFunctionException(int errorCode, optional String? errorMessage);
-    method public final int getErrorCategory();
-    method public final int getErrorCode();
     method public final String? getErrorMessage();
-    property public final int errorCategory;
-    property public final int errorCode;
     property public final String? errorMessage;
     field public static final androidx.appfunctions.AppFunctionException.Companion Companion;
-    field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8
-    field public static final int ERROR_CANCELLED = 2001; // 0x7d1
-    field public static final int ERROR_CATEGORY_APP = 3; // 0x3
-    field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1
-    field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2
-    field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0
-    field public static final int ERROR_DENIED = 1000; // 0x3e8
-    field public static final int ERROR_DISABLED = 1002; // 0x3ea
-    field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb
-    field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9
-    field public static final int ERROR_LIMIT_EXCEEDED = 1501; // 0x5dd
-    field public static final int ERROR_NOT_SUPPORTED = 3501; // 0xdad
-    field public static final int ERROR_PERMISSION_REQUIRED = 3500; // 0xdac
-    field public static final int ERROR_RESOURCE_ALREADY_EXISTS = 1502; // 0x5de
-    field public static final int ERROR_RESOURCE_NOT_FOUND = 1500; // 0x5dc
-    field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0
   }
 
   public static final class AppFunctionException.Companion {
-    property public static final int ERROR_APP_UNKNOWN_ERROR;
-    property public static final int ERROR_CANCELLED;
-    property public static final int ERROR_CATEGORY_APP;
-    property public static final int ERROR_CATEGORY_REQUEST_ERROR;
-    property public static final int ERROR_CATEGORY_SYSTEM;
-    property public static final int ERROR_CATEGORY_UNKNOWN;
-    property public static final int ERROR_DENIED;
-    property public static final int ERROR_DISABLED;
-    property public static final int ERROR_FUNCTION_NOT_FOUND;
-    property public static final int ERROR_INVALID_ARGUMENT;
-    property public static final int ERROR_LIMIT_EXCEEDED;
-    property public static final int ERROR_NOT_SUPPORTED;
-    property public static final int ERROR_PERMISSION_REQUIRED;
-    property public static final int ERROR_RESOURCE_ALREADY_EXISTS;
-    property public static final int ERROR_RESOURCE_NOT_FOUND;
-    property public static final int ERROR_SYSTEM_ERROR;
   }
 
-  public final class AppFunctionFunctionNotFoundException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionFunctionNotFoundException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionFunctionNotFoundException(optional String? errorMessage);
   }
 
-  public final class AppFunctionInvalidArgumentException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionInvalidArgumentException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionInvalidArgumentException(optional String? errorMessage);
   }
 
-  public final class AppFunctionLimitExceededException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionLimitExceededException extends androidx.appfunctions.AppFunctionRequestException {
     ctor public AppFunctionLimitExceededException(optional String? errorMessage);
   }
 
-  public final class AppFunctionNotSupportedException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionNotSupportedException extends androidx.appfunctions.AppFunctionAppException {
     ctor public AppFunctionNotSupportedException(optional String? errorMessage);
   }
 
-  public final class AppFunctionPermissionRequiredException extends androidx.appfunctions.AppFunctionException {
+  public final class AppFunctionPermissionRequiredException extends androidx.appfunctions.AppFunctionAppException {
     ctor public AppFunctionPermissionRequiredException(optional String? errorMessage);
   }
 
+  public abstract class AppFunctionRequestException extends androidx.appfunctions.AppFunctionException {
+  }
+
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface AppFunctionSerializable {
   }
 
-  public final class AppFunctionSystemException extends androidx.appfunctions.AppFunctionException {
-    ctor public AppFunctionSystemException(optional String? errorMessage);
+  public abstract class AppFunctionSystemException extends androidx.appfunctions.AppFunctionException {
+  }
+
+  public final class AppFunctionSystemUnknownException extends androidx.appfunctions.AppFunctionSystemException {
+    ctor public AppFunctionSystemUnknownException(optional String? errorMessage);
+  }
+
+  public final class AppFunctionUnknownException extends androidx.appfunctions.AppFunctionException {
+    ctor public AppFunctionUnknownException(int unknownErrorCode, optional String? errorMessage);
+    method public int getUnknownErrorCode();
+    property public final int unknownErrorCode;
   }
 
 }
diff --git a/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionExceptionTest.kt b/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionExceptionTest.kt
index 31108ed..a8cbef9 100644
--- a/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionExceptionTest.kt
+++ b/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionExceptionTest.kt
@@ -23,85 +23,10 @@
 
 class AppFunctionExceptionTest {
     @Test
-    fun testConstructor_withoutMessageAndExtras() {
-        val exception = AppFunctionException(AppFunctionException.ERROR_DENIED)
-
-        assertThat(exception.errorCode).isEqualTo(AppFunctionException.ERROR_DENIED)
-        assertThat(exception.errorMessage).isNull()
-        assertThat(exception.extras).isEqualTo(Bundle.EMPTY)
-    }
-
-    @Test
-    fun testConstructor_withoutExtras() {
-        val exception = AppFunctionException(AppFunctionException.ERROR_DENIED, "testMessage")
-
-        assertThat(exception.errorCode).isEqualTo(AppFunctionException.ERROR_DENIED)
-        assertThat(exception.errorMessage).isEqualTo("testMessage")
-        assertThat(exception.extras).isEqualTo(Bundle.EMPTY)
-    }
-
-    @Test
-    fun testConstructor() {
-        val extras = Bundle().apply { putString("testKey", "testValue") }
-        val exception =
-            AppFunctionException(AppFunctionException.ERROR_DENIED, "testMessage", extras)
-
-        assertThat(exception.errorCode).isEqualTo(AppFunctionException.ERROR_DENIED)
-        assertThat(exception.errorMessage).isEqualTo("testMessage")
-        assertThat(exception.extras.getString("testKey")).isEqualTo("testValue")
-    }
-
-    @Test
-    fun testErrorCategory_RequestError() {
-        assertThat(AppFunctionException(AppFunctionException.ERROR_DENIED).errorCategory)
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
-        assertThat(AppFunctionException(AppFunctionException.ERROR_INVALID_ARGUMENT).errorCategory)
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
-        assertThat(AppFunctionException(AppFunctionException.ERROR_DISABLED).errorCategory)
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
-        assertThat(
-                AppFunctionException(AppFunctionException.ERROR_FUNCTION_NOT_FOUND).errorCategory
-            )
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
-        assertThat(
-                AppFunctionException(AppFunctionException.ERROR_RESOURCE_NOT_FOUND).errorCategory
-            )
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
-        assertThat(AppFunctionException(AppFunctionException.ERROR_LIMIT_EXCEEDED).errorCategory)
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
-        assertThat(
-                AppFunctionException(AppFunctionException.ERROR_RESOURCE_ALREADY_EXISTS)
-                    .errorCategory
-            )
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
-    }
-
-    @Test
-    fun testErrorCategory_SystemError() {
-        assertThat(AppFunctionException(AppFunctionException.ERROR_SYSTEM_ERROR).errorCategory)
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_SYSTEM)
-        assertThat(AppFunctionException(AppFunctionException.ERROR_CANCELLED).errorCategory)
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_SYSTEM)
-    }
-
-    @Test
-    fun testErrorCategory_AppError() {
-        assertThat(AppFunctionException(AppFunctionException.ERROR_APP_UNKNOWN_ERROR).errorCategory)
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_APP)
-        assertThat(
-                AppFunctionException(AppFunctionException.ERROR_PERMISSION_REQUIRED).errorCategory
-            )
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_APP)
-        assertThat(AppFunctionException(AppFunctionException.ERROR_NOT_SUPPORTED).errorCategory)
-            .isEqualTo(AppFunctionException.ERROR_CATEGORY_APP)
-    }
-
-    @Test
     fun testTransformToPlatformExtensionsClass() {
         assumeAppFunctionExtensionLibraryAvailable()
         val extras = Bundle().apply { putString("testKey", "testValue") }
-        val exception =
-            AppFunctionException(AppFunctionException.ERROR_DENIED, "testMessage", extras)
+        val exception = AppFunctionDeniedException("testMessage", extras)
 
         val platformException = exception.toPlatformExtensionsClass()
 
@@ -111,20 +36,81 @@
     }
 
     @Test
-    fun testCreateFromPlatformExtensionsClass() {
+    fun testCreateFromPlatformExtensionsClass_knownClasses() {
+        assumeAppFunctionExtensionLibraryAvailable()
+
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_APP_UNKNOWN_ERROR,
+            AppFunctionAppUnknownException::class.java
+        )
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_PERMISSION_REQUIRED,
+            AppFunctionPermissionRequiredException::class.java
+        )
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_NOT_SUPPORTED,
+            AppFunctionNotSupportedException::class.java
+        )
+
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_DENIED,
+            AppFunctionDeniedException::class.java
+        )
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_INVALID_ARGUMENT,
+            AppFunctionInvalidArgumentException::class.java
+        )
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_DISABLED,
+            AppFunctionDisabledException::class.java
+        )
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_FUNCTION_NOT_FOUND,
+            AppFunctionFunctionNotFoundException::class.java
+        )
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_LIMIT_EXCEEDED,
+            AppFunctionLimitExceededException::class.java
+        )
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_RESOURCE_ALREADY_EXISTS,
+            AppFunctionElementAlreadyExistsException::class.java
+        )
+
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_SYSTEM_ERROR,
+            AppFunctionSystemUnknownException::class.java
+        )
+        testCreateFromPlatformExtensionsClass(
+            AppFunctionException.ERROR_CANCELLED,
+            AppFunctionCancelledException::class.java
+        )
+    }
+
+    @Test
+    fun testCreateFromPlatformExtensionsClass_unknownErrorCode() {
+        assumeAppFunctionExtensionLibraryAvailable()
+
+        testCreateFromPlatformExtensionsClass(123456, AppFunctionUnknownException::class.java)
+    }
+
+    private fun <E : AppFunctionException> testCreateFromPlatformExtensionsClass(
+        errorCode: Int,
+        exceptionClass: Class<E>
+    ) {
         assumeAppFunctionExtensionLibraryAvailable()
         val extras = Bundle().apply { putString("testKey", "testValue") }
         val platformException =
             com.android.extensions.appfunctions.AppFunctionException(
-                AppFunctionException.ERROR_DENIED,
+                errorCode,
                 "testMessage",
                 extras
             )
 
         val exception = AppFunctionException.fromPlatformExtensionsClass(platformException)
 
-        assertThat(exception).isInstanceOf(AppFunctionDeniedException::class.java)
-        assertThat(exception.errorCode).isEqualTo(AppFunctionException.ERROR_DENIED)
+        assertThat(exception).isInstanceOf(exceptionClass)
+        assertThat(exception.errorCode).isEqualTo(errorCode)
         assertThat(exception.errorMessage).isEqualTo("testMessage")
         assertThat(exception.extras.getString("testKey")).isEqualTo("testValue")
     }
diff --git a/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionSystemExceptionsTest.kt b/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionSystemExceptionsTest.kt
index d59f337..89ccbf0 100644
--- a/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionSystemExceptionsTest.kt
+++ b/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionSystemExceptionsTest.kt
@@ -22,9 +22,9 @@
 class AppFunctionSystemExceptionsTest {
     @Test
     fun testErrorCategory_SystemError() {
-        assertThat(AppFunctionSystemException().errorCode)
+        assertThat(AppFunctionSystemUnknownException().errorCode)
             .isEqualTo(AppFunctionException.ERROR_SYSTEM_ERROR)
-        assertThat(AppFunctionSystemException().errorCategory)
+        assertThat(AppFunctionSystemUnknownException().errorCategory)
             .isEqualTo(AppFunctionException.ERROR_CATEGORY_SYSTEM)
 
         assertThat(AppFunctionCancelledException().errorCode)
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
index 2e73ca0..441c6bb 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
@@ -17,19 +17,25 @@
 package androidx.appfunctions
 
 import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_APP
+
+/**
+ * Thrown when an error is caused by the app providing the function.
+ *
+ * <p>For example, the app crashed when the system is executing the request.
+ */
+public abstract class AppFunctionAppException
+internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
+    AppFunctionException(errorCode, errorMessage, extras)
 
 /**
  * Thrown when an unknown error occurred while processing the call in the AppFunctionService.
  *
  * <p>This error is thrown when the service is connected in the remote application but an unexpected
  * error is thrown from the bound application.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
  */
 public class AppFunctionAppUnknownException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_APP_UNKNOWN_ERROR, errorMessage, extras) {
+    AppFunctionAppException(ERROR_APP_UNKNOWN_ERROR, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
@@ -43,12 +49,10 @@
  *
  * <p> This is different from [AppFunctionDeniedException] in that the required permission is
  * missing from the target app, as opposed to the caller.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
  */
 public class AppFunctionPermissionRequiredException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_PERMISSION_REQUIRED, errorMessage, extras) {
+    AppFunctionAppException(ERROR_PERMISSION_REQUIRED, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
@@ -58,12 +62,10 @@
  *
  * <p>For example, a clock app might support updating timer properties such as label but may not
  * allow updating the timer's duration once the timer has already started.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
  */
 public class AppFunctionNotSupportedException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_NOT_SUPPORTED, errorMessage, extras) {
+    AppFunctionAppException(ERROR_NOT_SUPPORTED, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionException.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionException.kt
index 65b79ee..e273dd4 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionException.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionException.kt
@@ -25,10 +25,10 @@
  *
  * This exception can be used by the app to report errors to the caller.
  */
-public open class AppFunctionException
+public abstract class AppFunctionException
 internal constructor(
     /** The error code. */
-    @ErrorCode public val errorCode: Int,
+    @ErrorCode internal val errorCode: Int,
     /** The error message. */
     public val errorMessage: String?,
     internal val extras: Bundle
@@ -65,7 +65,7 @@
      * error category.
      */
     @ErrorCategory
-    public val errorCategory: Int =
+    internal val errorCategory: Int =
         when (errorCode) {
             in 1000..1999 -> ERROR_CATEGORY_REQUEST_ERROR
             in 2000..2999 -> ERROR_CATEGORY_SYSTEM
@@ -126,26 +126,28 @@
                         exception.errorMessage,
                         exception.extras
                     )
+                ERROR_SYSTEM_ERROR ->
+                    AppFunctionSystemUnknownException(exception.errorMessage, exception.extras)
                 ERROR_CANCELLED ->
                     AppFunctionCancelledException(exception.errorMessage, exception.extras)
-                ERROR_APP_UNKNOWN_ERROR, ->
-                    AppFunctionCancelledException(exception.errorMessage, exception.extras)
+                ERROR_APP_UNKNOWN_ERROR ->
+                    AppFunctionAppUnknownException(exception.errorMessage, exception.extras)
                 ERROR_PERMISSION_REQUIRED ->
                     AppFunctionPermissionRequiredException(exception.errorMessage, exception.extras)
                 ERROR_NOT_SUPPORTED ->
                     AppFunctionNotSupportedException(exception.errorMessage, exception.extras)
                 else ->
-                    AppFunctionException(
+                    AppFunctionUnknownException(
                         exception.errorCode,
                         exception.errorMessage,
-                        exception.extras
+                        exception.extras,
                     )
             }
         }
 
         // Error categories
         /** The error category is unknown. */
-        public const val ERROR_CATEGORY_UNKNOWN: Int = 0
+        internal const val ERROR_CATEGORY_UNKNOWN: Int = 0
 
         /**
          * The error is caused by the app requesting a function execution.
@@ -155,7 +157,7 @@
          *
          * <p>Errors in the category fall in the range 1000-1999 inclusive.
          */
-        public const val ERROR_CATEGORY_REQUEST_ERROR: Int = 1
+        internal const val ERROR_CATEGORY_REQUEST_ERROR: Int = 1
 
         /**
          * The error is caused by an issue in the system.
@@ -164,7 +166,7 @@
          *
          * <p>Errors in the category fall in the range 2000-2999 inclusive.
          */
-        public const val ERROR_CATEGORY_SYSTEM: Int = 2
+        internal const val ERROR_CATEGORY_SYSTEM: Int = 2
 
         /**
          * The error is caused by the app providing the function.
@@ -173,7 +175,7 @@
          *
          * <p>Errors in the category fall in the range 3000-3999 inclusive.
          */
-        public const val ERROR_CATEGORY_APP: Int = 3
+        internal const val ERROR_CATEGORY_APP: Int = 3
 
         // Error codes
         /**
@@ -184,7 +186,7 @@
          *
          * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
          */
-        public const val ERROR_DENIED: Int = 1000
+        internal const val ERROR_DENIED: Int = 1000
 
         /**
          * The caller supplied invalid arguments to the execution request.
@@ -193,21 +195,21 @@
          *
          * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
          */
-        public const val ERROR_INVALID_ARGUMENT: Int = 1001
+        internal const val ERROR_INVALID_ARGUMENT: Int = 1001
 
         /**
          * The caller tried to execute a disabled app function.
          *
          * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
          */
-        public const val ERROR_DISABLED: Int = 1002
+        internal const val ERROR_DISABLED: Int = 1002
 
         /**
          * The caller tried to execute a function that does not exist.
          *
          * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
          */
-        public const val ERROR_FUNCTION_NOT_FOUND: Int = 1003
+        internal const val ERROR_FUNCTION_NOT_FOUND: Int = 1003
 
         // SDK-defined error codes in the [ERROR_CATEGORY_REQUEST_ERROR] category start from 1500.
         /**
@@ -215,14 +217,14 @@
          *
          * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
          */
-        public const val ERROR_RESOURCE_NOT_FOUND: Int = 1500
+        internal const val ERROR_RESOURCE_NOT_FOUND: Int = 1500
 
         /**
          * The caller exceeded the allowed request rate.
          *
          * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
          */
-        public const val ERROR_LIMIT_EXCEEDED: Int = 1501
+        internal const val ERROR_LIMIT_EXCEEDED: Int = 1501
 
         /**
          * The caller tried to create a resource/entity that already exists or has conflicts with
@@ -230,14 +232,14 @@
          *
          * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
          */
-        public const val ERROR_RESOURCE_ALREADY_EXISTS: Int = 1502
+        internal const val ERROR_RESOURCE_ALREADY_EXISTS: Int = 1502
 
         /**
          * An internal unexpected error coming from the system.
          *
          * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
          */
-        public const val ERROR_SYSTEM_ERROR: Int = 2000
+        internal const val ERROR_SYSTEM_ERROR: Int = 2000
 
         /**
          * The operation was cancelled. Use this error code to report that a cancellation is done
@@ -245,7 +247,7 @@
          *
          * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
          */
-        public const val ERROR_CANCELLED: Int = 2001
+        internal const val ERROR_CANCELLED: Int = 2001
 
         /**
          * An unknown error occurred while processing the call in the AppFunctionService.
@@ -255,7 +257,7 @@
          *
          * <p>This error is in the [ERROR_CATEGORY_APP] category.
          */
-        public const val ERROR_APP_UNKNOWN_ERROR: Int = 3000
+        internal const val ERROR_APP_UNKNOWN_ERROR: Int = 3000
 
         // SDK-defined error codes in the [ERROR_CATEGORY_APP] category start from 3500.
         /**
@@ -271,7 +273,7 @@
          *
          * <p>This error is in the [ERROR_CATEGORY_APP] category.
          */
-        public const val ERROR_PERMISSION_REQUIRED: Int = 3500
+        internal const val ERROR_PERMISSION_REQUIRED: Int = 3500
 
         /**
          * Indicates the action is not supported by the app.
@@ -282,6 +284,25 @@
          *
          * <p>This error is in the [ERROR_CATEGORY_APP] category.
          */
-        public const val ERROR_NOT_SUPPORTED: Int = 3501
+        internal const val ERROR_NOT_SUPPORTED: Int = 3501
     }
 }
+
+/**
+ * Thrown when an unknown error has occurred.
+ *
+ * <p> This Exception is used when the error doesn't belong to any other AppFunctionException. Note
+ * that this different from [AppFunctionAppUnknownException], in that the error wasn't necessarily
+ * caused by the app.
+ */
+public class AppFunctionUnknownException
+internal constructor(
+    public val unknownErrorCode: Int,
+    errorMessage: String? = null,
+    extras: Bundle
+) : AppFunctionException(unknownErrorCode, errorMessage, extras) {
+    public constructor(
+        unknownErrorCode: Int,
+        errorMessage: String? = null
+    ) : this(unknownErrorCode, errorMessage, Bundle.EMPTY)
+}
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
index 2a4377b..fc16714 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
@@ -17,19 +17,26 @@
 package androidx.appfunctions
 
 import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_REQUEST_ERROR
+
+/**
+ * Thrown when the error is caused by the app requesting a function execution.
+ *
+ * <p>For example, the caller provided invalid parameters in the execution request e.g. an invalid
+ * function ID.
+ */
+public abstract class AppFunctionRequestException
+internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
+    AppFunctionException(errorCode, errorMessage, extras)
 
 /**
  * Thrown when the caller does not have the permission to execute an app function.
  *
  * <p> This is different from [AppFunctionPermissionRequiredException] in that the caller is missing
  * this specific permission, as opposed to the target app missing a permission.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
  */
 public class AppFunctionDeniedException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_DENIED, errorMessage, extras) {
+    AppFunctionRequestException(ERROR_DENIED, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
@@ -38,13 +45,11 @@
  * Thrown when the caller supplied invalid arguments to ExecuteAppFunctionRequest's parameters.
  *
  * <p>This error may be considered similar to [IllegalArgumentException].
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
  */
 // TODO(b/389738031): add reference to ExecuteAppFunctionRequest's builder when it is added.
 public class AppFunctionInvalidArgumentException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_INVALID_ARGUMENT, errorMessage, extras) {
+    AppFunctionRequestException(ERROR_INVALID_ARGUMENT, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
@@ -53,49 +58,35 @@
  * Thrown when the caller tried to execute a disabled app function. An app function can be enabled
  * at runtime through the AppFunctionManager or by setting enabledByDefault=true in the AppFunction
  * annotation.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
  */
 // TODO(b/389738031): add reference to setAppFunctionEnabled and @AppFunction when they are added.
 public class AppFunctionDisabledException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_DISABLED, errorMessage, extras) {
+    AppFunctionRequestException(ERROR_DISABLED, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
 
-/**
- * Thrown when the caller tries to execute a function that does not exist.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller tries to execute a function that does not exist. */
 public class AppFunctionFunctionNotFoundException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_FUNCTION_NOT_FOUND, errorMessage, extras) {
+    AppFunctionRequestException(ERROR_FUNCTION_NOT_FOUND, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
 
-/**
- * Thrown when the caller tried to request a resource/entity that does not exist.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller tried to request a resource/entity that does not exist. */
 public class AppFunctionElementNotFoundException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_RESOURCE_NOT_FOUND, errorMessage, extras) {
+    AppFunctionRequestException(ERROR_RESOURCE_NOT_FOUND, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
 
-/**
- * Thrown when the caller exceeded the allowed request rate.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller exceeded the allowed request rate. */
 public class AppFunctionLimitExceededException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_LIMIT_EXCEEDED, errorMessage, extras) {
+    AppFunctionRequestException(ERROR_LIMIT_EXCEEDED, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
@@ -103,12 +94,10 @@
 /**
  * Thrown when the caller tried to create a resource/entity that already exists or has conflicts
  * with existing resource/entity.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
  */
 public class AppFunctionElementAlreadyExistsException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_RESOURCE_ALREADY_EXISTS, errorMessage, extras) {
+    AppFunctionRequestException(ERROR_RESOURCE_ALREADY_EXISTS, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
index fbe8bcf..66596b4 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
@@ -17,30 +17,32 @@
 package androidx.appfunctions
 
 import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_SYSTEM
 
 /**
  * Thrown when an internal unexpected error comes from the system.
  *
  * <p>For example, the AppFunctionService implementation is not found by the system.
- *
- * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
  */
-public class AppFunctionSystemException
+public abstract class AppFunctionSystemException
+internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
+    AppFunctionException(errorCode, errorMessage, extras)
+
+/**
+ * Thrown when an internal unexpected error comes from the system.
+ *
+ * <p>For example, the AppFunctionService implementation is not found by the system.
+ */
+public class AppFunctionSystemUnknownException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_SYSTEM_ERROR, errorMessage, extras) {
+    AppFunctionSystemException(ERROR_SYSTEM_ERROR, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
 
-/**
- * Thrown when an operation was cancelled.
- *
- * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
- */
+/** Thrown when an operation was cancelled. */
 public class AppFunctionCancelledException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(ERROR_CANCELLED, errorMessage, extras) {
+    AppFunctionSystemException(ERROR_CANCELLED, errorMessage, extras) {
 
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManager.kt
index 0a28700..1fcd4f7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManager.kt
@@ -25,6 +25,7 @@
 import androidx.camera.camera2.pipe.core.PruningProcessingQueue
 import androidx.camera.camera2.pipe.core.PruningProcessingQueue.Companion.processIn
 import androidx.camera.camera2.pipe.core.Threads
+import androidx.camera.camera2.pipe.core.Token
 import androidx.camera.camera2.pipe.core.WakeLock
 import androidx.camera.camera2.pipe.graph.GraphListener
 import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
@@ -138,8 +139,14 @@
         }
     }
 
-    suspend fun connectTo(virtualCameraState: VirtualCameraState) {
-        val token = wakelock.acquire()
+    // Acquire this ActiveCamera. This ensures the camera stay opened for as long as the token is
+    // held. This is important for camera open scenarios where the device manager should acquire
+    // the ActiveCamera for the duration under which it's processing an open request.
+    fun acquire() = wakelock.acquire()
+
+    // TODO: b/389758537, b/390530866 - Make Token non-nullable. If we cannot acquire a token, the
+    //  ActiveCamera has issued a RequestClose for this ActiveCamera already.
+    suspend fun connectTo(virtualCameraState: VirtualCameraState, token: Token?) {
         val previous = current
         current = virtualCameraState
 
@@ -179,8 +186,18 @@
     private val queue =
         PruningProcessingQueue<CameraRequest>(prune = ::prune) { process(it) }.processIn(scope)
     private val activeCameras: MutableSet<ActiveCamera> = mutableSetOf()
-    private val pendingRequestOpens = mutableListOf<RequestOpen>()
-    private val pendingRequestOpenActiveCameraMap = mutableMapOf<RequestOpen, ActiveCamera>()
+
+    // PendingRequestOpen stores the context information for the pending RequestOpens to be
+    // connected in concurrent camera scenarios. It contains the request itself, the active camera
+    // it should be connected with, and the usage token for the active camera. The token should
+    // always be closed if the context is removed without being connected to a VirtualCamera.
+    private class PendingRequestOpen(
+        val request: RequestOpen,
+        val activeCamera: ActiveCamera,
+        val token: Token,
+    )
+
+    private val pendingRequestOpens = mutableListOf<PendingRequestOpen>()
 
     override fun open(
         cameraId: CameraId,
@@ -358,12 +375,9 @@
         if (camerasToClose.isNotEmpty()) {
             // Shutdown of cameras should always happen first (and suspend until complete)
             activeCameras.removeAll(camerasToClose)
-            for (requestOpen in pendingRequestOpens) {
-                if (camerasToClose.contains(pendingRequestOpenActiveCameraMap[requestOpen])) {
-                    pendingRequestOpens.remove(requestOpen)
-                    pendingRequestOpenActiveCameraMap.remove(requestOpen)
-                }
-            }
+            disconnectPendingRequestOpens(
+                pendingRequestOpens.filter { camerasToClose.contains(it.activeCamera) }
+            )
             for (camera in camerasToClose) {
                 camera.close()
             }
@@ -374,51 +388,37 @@
 
         // Step 2: Open the camera if not opened already.
         camera2ErrorProcessor.setActiveVirtualCamera(cameraIdToOpen, request.virtualCamera)
-        var realCamera = activeCameras.firstOrNull { it.cameraId == cameraIdToOpen }
-        if (realCamera == null) {
-            val openResult =
-                openCameraWithRetry(
-                    cameraIdToOpen,
-                    request.sharedCameraIds,
-                    request.isForegroundObserver,
-                    scope,
-                )
-            when (openResult) {
-                is OpenVirtualCameraResult.Success -> {
-                    realCamera = openResult.activeCamera
-                    activeCameras.add(realCamera)
-                }
-                is OpenVirtualCameraResult.Error -> {
-                    request.virtualCamera.disconnect(openResult.lastCameraError)
-                    return
-                }
-            }
+        val result = retrieveActiveCamera(cameraIdToOpen, request)
+        if (result == null) {
+            Log.error { "Failed to retrieve active camera for $cameraIdToOpen" }
+            return
         }
+        val realCamera = result.activeCamera
+        val realCameraToken = result.token
 
         // Step 3: Connect the opened camera(s).
         if (request.sharedCameraIds.isNotEmpty()) {
-            // Both sharedCameraIds and activeCameras are small collections. Looping over them
-            // in what equates to nested for-loops are actually going to be more efficient than
-            // say, replacing activeCameras with a hashmap.
+            // Both sharedCameraIds and pendingRequestOpenContexts are small collections. Looping
+            // over them in what equates to nested for-loops are actually going to be more efficient
+            // than say, replacing activeCameras with a hashmap.
             if (
                 request.sharedCameraIds.all { cameraId ->
-                    activeCameras.any { it.cameraId == cameraId }
+                    pendingRequestOpens.any { it.activeCamera.cameraId == cameraId }
                 }
             ) {
                 // If the camera of the request and the cameras it is shared with have been
                 // opened, we can connect the ActiveCameras.
                 check(!request.isPrewarm)
-                realCamera.connectTo(request.virtualCamera)
-                connectPendingRequestOpens(request.sharedCameraIds)
+                realCamera.connectTo(request.virtualCamera, realCameraToken)
+                connectPendingRequestOpens(request.sharedCameraIds.toSet())
             } else {
                 // Else, save the request in the pending request queue, and connect the request
                 // once other cameras are opened.
-                pendingRequestOpens.add(request)
-                pendingRequestOpenActiveCameraMap[request] = realCamera
+                pendingRequestOpens.add(PendingRequestOpen(request, realCamera, realCameraToken))
             }
         } else {
             if (!request.isPrewarm) {
-                realCamera.connectTo(request.virtualCamera)
+                realCamera.connectTo(request.virtualCamera, realCameraToken)
             }
         }
     }
@@ -430,16 +430,13 @@
         if (activeCameras.contains(request.activeCamera)) {
             activeCameras.remove(request.activeCamera)
         }
-        for (requestOpen in pendingRequestOpens) {
-            // Edge case: There is a possibility that we receive RequestClose after a RequestOpen
-            // for concurrent cameras has been processed. As such, we don't want to close the
-            // ActiveCamera newly created by the RequestOpen, but only the one RequestClose is
-            // aiming to close.
-            if (pendingRequestOpenActiveCameraMap[requestOpen] == request.activeCamera) {
-                pendingRequestOpens.remove(requestOpen)
-                pendingRequestOpenActiveCameraMap.remove(requestOpen)
-            }
-        }
+
+        // Edge case: There is a possibility that we receive RequestClose after a RequestOpen for
+        // concurrent cameras has been processed. As such, we don't want to close the ActiveCamera
+        // newly created by the RequestOpen, but only the one RequestClose is aiming to close.
+        disconnectPendingRequestOpens(
+            pendingRequestOpens.filter { it.activeCamera == request.activeCamera }
+        )
         request.activeCamera.close()
         request.activeCamera.awaitClosed()
     }
@@ -448,12 +445,9 @@
         val cameraId = request.activeCameraId
         Log.info { "PruningCamera2DeviceManager#processRequestCloseById(${request.activeCameraId}" }
 
-        for (requestOpen in pendingRequestOpens) {
-            if (requestOpen.virtualCamera.cameraId == cameraId) {
-                pendingRequestOpens.remove(requestOpen)
-                pendingRequestOpenActiveCameraMap.remove(requestOpen)
-            }
-        }
+        disconnectPendingRequestOpens(
+            pendingRequestOpens.filter { it.request.virtualCamera.cameraId == cameraId }
+        )
         val activeCamera = activeCameras.firstOrNull { it.cameraId == cameraId }
         if (activeCamera != null) {
             activeCameras.remove(activeCamera)
@@ -466,8 +460,7 @@
     private suspend fun processRequestCloseAll(requestCloseAll: RequestCloseAll) {
         Log.info { "PruningCamera2DeviceManager#processRequestCloseAll()" }
 
-        pendingRequestOpens.clear()
-        pendingRequestOpenActiveCameraMap.clear()
+        disconnectPendingRequestOpens(pendingRequestOpens)
         for (activeCamera in activeCameras) {
             activeCamera.close()
         }
@@ -478,6 +471,59 @@
         requestCloseAll.deferred.complete(Unit)
     }
 
+    private suspend fun retrieveActiveCamera(
+        cameraId: CameraId,
+        requestOpen: RequestOpen,
+    ): RetrieveActiveCameraResult? {
+        var realCamera: ActiveCamera? = null
+        var realCameraToken: Token? = null
+        for (activeCamera in activeCameras) {
+            if (activeCamera.cameraId == cameraId) {
+                // Important: When we retrieve an active camera, there is a chance it has already
+                // reached its idle timeout, but we haven't processed the close request and remove
+                // it from the list. It's also possible that after we fetch this camera, this camera
+                // then gets closed in parallel by the idle timeout. Therefore, here we should
+                // acquire a token from this active camera to mark it as used and keep it opened.
+                val token = activeCamera.acquire()
+                if (token != null) {
+                    realCamera = activeCamera
+                    realCameraToken = token
+                    break
+                } else {
+                    // This ActiveCamera is already disconnected (i.e., WakeLock closed). Make sure
+                    // the camera is closed before reopening.
+                    activeCamera.close()
+                    activeCamera.awaitClosed()
+                    activeCameras.remove(activeCamera)
+                }
+            }
+        }
+        if (realCamera == null) {
+            val openResult =
+                openCameraWithRetry(
+                    cameraId,
+                    requestOpen.sharedCameraIds,
+                    requestOpen.isForegroundObserver,
+                    scope,
+                )
+            when (openResult) {
+                is OpenVirtualCameraResult.Success -> {
+                    Log.info { "PruningCameraDeviceManager: $cameraId opened successfully" }
+                    realCamera = openResult.activeCamera
+                    // Acquire a token to mark this active camera as used.
+                    realCameraToken = checkNotNull(realCamera.acquire())
+                    activeCameras.add(realCamera)
+                }
+                is OpenVirtualCameraResult.Error -> {
+                    Log.info { "PruningCameraDeviceManager: Failed to open $cameraId" }
+                    requestOpen.virtualCamera.disconnect(openResult.lastCameraError)
+                    return null
+                }
+            }
+        }
+        return RetrieveActiveCameraResult(realCamera, checkNotNull(realCameraToken))
+    }
+
     private suspend fun openCameraWithRetry(
         cameraId: CameraId,
         sharedCameraIds: List<CameraId>,
@@ -509,23 +555,32 @@
         )
     }
 
-    private suspend fun connectPendingRequestOpens(cameraIds: List<CameraId>) {
-        val requestOpensToRemove = mutableListOf<RequestOpen>()
-        val requestOpens =
-            pendingRequestOpens.filter { cameraIds.contains(it.virtualCamera.cameraId) }
-        for (request in requestOpens) {
-            // If the request is shared with this pending request, then we should be
-            // able to connect this pending request too, since we don't allow
-            // overlapping.
+    private suspend fun connectPendingRequestOpens(cameraIds: Set<CameraId>) {
+        val filteredPendingRequestOpens =
+            pendingRequestOpens.filter { cameraIds.contains(it.request.virtualCamera.cameraId) }
+        for (pendingRequestOpen in filteredPendingRequestOpens) {
+            val request = pendingRequestOpen.request
+
+            // If the request is shared with this pending request, then we should be able to connect
+            // this pending request too, since we don't allow overlapping.
             val allCameraIds = listOf(request.virtualCamera.cameraId) + request.sharedCameraIds
             check(allCameraIds.all { cameraId -> activeCameras.any { it.cameraId == cameraId } })
 
-            val realCamera = activeCameras.find { it.cameraId == request.virtualCamera.cameraId }
-            checkNotNull(realCamera)
-            realCamera.connectTo(request.virtualCamera)
-            requestOpensToRemove.add(request)
+            pendingRequestOpen.activeCamera.connectTo(
+                request.virtualCamera,
+                pendingRequestOpen.token
+            )
+            pendingRequestOpens.remove(pendingRequestOpen)
         }
-        pendingRequestOpens.removeAll(requestOpensToRemove)
+    }
+
+    private suspend fun disconnectPendingRequestOpens(
+        pendingRequestOpensToDisconnect: List<PendingRequestOpen>,
+    ) {
+        for (pendingRequestOpen in pendingRequestOpensToDisconnect) {
+            pendingRequestOpen.token.release()
+            pendingRequestOpens.remove(pendingRequestOpen)
+        }
     }
 
     private inline fun <T> List<T>.firstFromIndexOrNull(
@@ -548,6 +603,8 @@
         return removedElements
     }
 
+    private class RetrieveActiveCameraResult(val activeCamera: ActiveCamera, val token: Token)
+
     private sealed interface OpenVirtualCameraResult {
         data class Success(val activeCamera: ActiveCamera) : OpenVirtualCameraResult
 
@@ -788,7 +845,7 @@
                     // If the camera of the request and the cameras it is shared with have been
                     // opened, we can connect the ActiveCameras.
                     check(!request.isPrewarm)
-                    realCamera.connectTo(request.virtualCamera)
+                    realCamera.connectTo(request.virtualCamera, realCamera.acquire())
                     connectPendingRequestOpens(request.sharedCameraIds)
                 } else {
                     // Else, save the request in the pending request queue, and connect the request
@@ -797,7 +854,7 @@
                 }
             } else {
                 if (!request.isPrewarm) {
-                    realCamera.connectTo(request.virtualCamera)
+                    realCamera.connectTo(request.virtualCamera, realCamera.acquire())
                 }
             }
             requests.remove(request)
@@ -850,7 +907,7 @@
 
             val realCamera = activeCameras.find { it.cameraId == request.virtualCamera.cameraId }
             checkNotNull(realCamera)
-            realCamera.connectTo(request.virtualCamera)
+            realCamera.connectTo(request.virtualCamera, realCamera.acquire())
             requestOpensToRemove.add(request)
         }
         pendingRequestOpens.removeAll(requestOpensToRemove)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt
index cb7ac0b..d8b0924 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt
@@ -315,7 +315,7 @@
             val virtualCamera1 =
                 deviceManager.open(cameraId0, listOf(cameraId1), fakeGraphListener1, false) { true }
             assertNotNull(virtualCamera1)
-            // Advance time by just a bit to allow coroutines to finish but not closing the camera.
+            // Advance time by just a bit to allow coroutines to finish.
             advanceTimeBy(100)
 
             assertEquals(fakeRetryingCameraStateOpener.androidCameraStates.size, 1)
@@ -344,6 +344,42 @@
         }
 
     @Test
+    fun pendingCamerasShouldBeHeldWhenOpeningConcurrentCameras() =
+        testScope.runTest {
+            val virtualCamera1 =
+                deviceManager.open(cameraId0, listOf(cameraId1), fakeGraphListener1, false) { true }
+            assertNotNull(virtualCamera1)
+            // Advance until idle. This is the only but notable difference between this and the
+            // prior test. Note that here because the request is still pending, the active camera
+            // should not be closed.
+            advanceUntilIdle()
+
+            assertEquals(fakeRetryingCameraStateOpener.androidCameraStates.size, 1)
+            val androidCameraState1 = fakeRetryingCameraStateOpener.androidCameraStates.first()
+            androidCameraState1.onOpened(fakeCameraDevice0)
+            advanceUntilIdle()
+
+            // Since camera 1 is not yet opened, the virtual camera should not be connected yet.
+            var virtualCameraState1 = virtualCamera1.value
+            assertIsNot<CameraStateOpen>(virtualCameraState1)
+
+            val virtualCamera2 =
+                deviceManager.open(cameraId1, listOf(cameraId0), fakeGraphListener2, false) { true }
+            assertNotNull(virtualCamera2)
+            advanceUntilIdle()
+
+            assertEquals(fakeRetryingCameraStateOpener.androidCameraStates.size, 2)
+            val androidCameraState2 = fakeRetryingCameraStateOpener.androidCameraStates.last()
+            androidCameraState2.onOpened(fakeCameraDevice1)
+            advanceUntilIdle()
+
+            virtualCameraState1 = virtualCamera1.value
+            assertIs<CameraStateOpen>(virtualCameraState1)
+            val virtualCameraState2 = virtualCamera2.value
+            assertIs<CameraStateOpen>(virtualCameraState2)
+        }
+
+    @Test
     fun singleCameraShouldBeClosedWhenConcurrentCamerasAreRequested() =
         testScope.runTest {
             // First open camera 0 in regular (single) camera mode.
@@ -395,7 +431,7 @@
             val virtualCamera1 =
                 deviceManager.open(cameraId0, listOf(cameraId1), fakeGraphListener1, false) { true }
             assertNotNull(virtualCamera1)
-            // Advance time by just a bit to allow coroutines to finish but not closing the camera.
+            // Advance time by just a bit to allow coroutines to finish.
             advanceTimeBy(100)
 
             assertEquals(fakeRetryingCameraStateOpener.androidCameraStates.size, 1)
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
index de994f6..8bcdb8c 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
@@ -23,6 +23,7 @@
 
 import androidx.camera.camera2.Camera2Config;
 import androidx.camera.camera2.internal.util.TestUtil;
+import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraXConfig;
 import androidx.camera.core.ImageAnalysis;
@@ -88,7 +89,8 @@
         mCamera = CameraUtil.createCameraAndAttachUseCase(context, cameraSelector, imageAnalysis);
         mCameraControl = TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
         mTorchControl = mCameraControl.getTorchControl();
-        mIsTorchStrengthSupported = mCamera.getCameraInfo().getMaxTorchStrengthLevel() > 1;
+        mIsTorchStrengthSupported = mCamera.getCameraInfo().getMaxTorchStrengthLevel()
+                != CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED;
     }
 
     @After
@@ -118,7 +120,7 @@
 
     @Test(timeout = 5000L)
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOnLevel()
+    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOn()
             throws ExecutionException, InterruptedException {
         assumeTrue(mIsTorchStrengthSupported);
 
@@ -132,7 +134,7 @@
 
     @Test(timeout = 5000L)
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOffLevel()
+    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOff()
             throws ExecutionException, InterruptedException {
         assumeTrue(mIsTorchStrengthSupported);
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 8a2a439..d6321f2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -516,6 +516,10 @@
             return Futures.immediateFailedFuture(
                     new OperationCanceledException("Camera is not active."));
         }
+        if (!mCameraCharacteristics.isTorchStrengthLevelSupported()) {
+            return Futures.immediateFailedFuture(new UnsupportedOperationException(
+                    "The device doesn't support configuring torch strength level."));
+        }
         if (torchStrengthLevel < 1
                 || torchStrengthLevel > mCameraCharacteristics.getMaxTorchStrengthLevel()) {
             return Futures.immediateFailedFuture(new IllegalArgumentException(
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index aeb7058..4804b521 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -719,9 +719,11 @@
     }
 
     @Override
-    @IntRange(from = 1)
+    @IntRange(from = 0)
     public int getMaxTorchStrengthLevel() {
-        return mCameraCharacteristicsCompat.getMaxTorchStrengthLevel();
+        return mCameraCharacteristicsCompat.isTorchStrengthLevelSupported()
+                ? mCameraCharacteristicsCompat.getMaxTorchStrengthLevel()
+                : TORCH_STRENGTH_LEVEL_UNSUPPORTED;
     }
 
     @Override
@@ -730,7 +732,9 @@
             if (mCamera2CameraControlImpl == null) {
                 if (mRedirectTorchStrengthLiveData == null) {
                     mRedirectTorchStrengthLiveData = new RedirectableLiveData<>(
-                            mCameraCharacteristicsCompat.getDefaultTorchStrengthLevel());
+                            mCameraCharacteristicsCompat.isTorchStrengthLevelSupported()
+                                    ? mCameraCharacteristicsCompat.getDefaultTorchStrengthLevel()
+                                    : TORCH_STRENGTH_LEVEL_UNSUPPORTED);
                 }
                 return mRedirectTorchStrengthLiveData;
             }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index 95d9e1c..49ef6d6 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -16,7 +16,6 @@
 
 package androidx.camera.camera2.internal;
 
-import android.annotation.SuppressLint;
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCaptureSession;
@@ -68,8 +67,6 @@
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -1067,7 +1064,8 @@
                 continue;
             }
             List<OutputConfiguration> outputConfigurations =
-                    createInstancesForMultiResolutionOutput(streamInfos, imageFormat);
+                    OutputConfiguration.createInstancesForMultiResolutionOutput(streamInfos,
+                            imageFormat);
             if (outputConfigurations != null) {
                 for (SessionConfig.OutputConfig outputConfig : groupIdToOutputConfigsMap.get(
                         groupId)) {
@@ -1082,31 +1080,6 @@
         return outputConfigToOutputConfigurationCompatMap;
     }
 
-    /**
-     * Use java reflection to access the API so that we don't need to upgrade compileSdk as 35 in
-     * the release branch. When this method is invoked, the API has become public on the device. It
-     * won't cause the problem about accessing the non-SDK API.
-     */
-    /** @noinspection unchecked */
-    @SuppressLint("BanUncheckedReflection")
-    @SuppressWarnings("unchecked")
-    @RequiresApi(35)
-    private static @Nullable List<OutputConfiguration> createInstancesForMultiResolutionOutput(
-            @NonNull List<MultiResolutionStreamInfo> streamInfos, int format) {
-        // TODO(b/376185185): Invoke the API directly after the androidx code base upgrades to
-        //  compile by API 35 SDK.
-        try {
-            Method createInstanceMethod = OutputConfiguration.class.getMethod(
-                    "createInstancesForMultiResolutionOutput", Collection.class, int.class);
-            return (List<OutputConfiguration>) createInstanceMethod.invoke(null, streamInfos,
-                    format);
-        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
-            Logger.e(TAG,
-                    "Failed to create instances for multi-resolution output, " + e.getMessage());
-            return null;
-        }
-    }
-
     // Debugging note: these states are kept in ordinal order. Any additions or changes should try
     // to maintain the same order such that the highest ordinal is the state of largest resource
     // utilization.
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index 7ea9db9..1bb729d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -103,10 +103,10 @@
         mExecutor = executor;
 
         mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics::get);
-        mIsTorchStrengthSupported =
-                mHasFlashUnit && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
-                        && cameraCharacteristics.getMaxTorchStrengthLevel() > 1;
-        mDefaultTorchStrength = cameraCharacteristics.getDefaultTorchStrengthLevel();
+        mIsTorchStrengthSupported = cameraCharacteristics.isTorchStrengthLevelSupported();
+        mDefaultTorchStrength = mHasFlashUnit && mIsTorchStrengthSupported
+                ? cameraCharacteristics.getDefaultTorchStrengthLevel()
+                : Camera2CameraInfoImpl.TORCH_STRENGTH_LEVEL_UNSUPPORTED;
         mTargetTorchStrength = mDefaultTorchStrength;
         mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
         mTorchStrength = new MutableLiveData<>(mDefaultTorchStrength);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
index 1f2cc60..766cfec 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
@@ -22,6 +22,7 @@
 import android.os.Build;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.camera2.internal.compat.workaround.OutputSizesCorrector;
 
@@ -135,23 +136,40 @@
      */
     public int getDefaultTorchStrengthLevel() {
         Integer defaultLevel = null;
-        if (Build.VERSION.SDK_INT >= 35) {
+        if (hasFlashUnit() && Build.VERSION.SDK_INT >= 35) {
             defaultLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_DEFAULT_LEVEL);
         }
+        // The framework returns 1 when the device doesn't support configuring torch strength. So
+        // also return 1 if the device doesn't have flash unit or is unable to provide the
+        // information.
         return defaultLevel == null ? 1 : defaultLevel;
     }
 
     /**
      * Returns the maximum torch strength level.
      */
+    @IntRange(from = 1)
     public int getMaxTorchStrengthLevel() {
         Integer maxLevel = null;
-        if (Build.VERSION.SDK_INT >= 35) {
+        if (hasFlashUnit() && Build.VERSION.SDK_INT >= 35) {
             maxLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_MAX_LEVEL);
         }
+        // The framework returns 1 when the device doesn't support configuring torch strength. So
+        // also return 1 if the device doesn't have flash unit or is unable to provide the
+        // information.
         return maxLevel == null ? 1 : maxLevel;
     }
 
+    public boolean isTorchStrengthLevelSupported() {
+        return hasFlashUnit() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
+                && getMaxTorchStrengthLevel() > 1;
+    }
+
+    private boolean hasFlashUnit() {
+        Boolean flashInfoAvailable = get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+        return flashInfoAvailable != null && flashInfoAvailable;
+    }
+
     /**
      * Obtains the {@link StreamConfigurationMapCompat} which contains the output sizes related
      * workarounds in it.
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index e7fcc70..1d87d00 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -916,6 +916,30 @@
         assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(CAMERA0_MAX_TORCH_STRENGTH);
     }
 
+    @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    public void apiVersionMet_canReturnMaxTorchStrengthUnsupported()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(
+                CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    public void apiVersionMet_canReturnTorchStrengthUnsupported()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.getTorchStrengthLevel().getValue()).isEqualTo(
+                CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
+    }
+
     @Config(minSdk = 33)
     @Test
     public void apiVersionMet_canReturnSupportedDynamicRanges_fromFullySpecified()
@@ -944,13 +968,14 @@
 
     @Config(maxSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM - 1)
     @Test
-    public void apiVersionNotMet_returnMaxTorchStrengthOne()
+    public void apiVersionNotMet_returnMaxTorchStrengthUnsupported()
             throws CameraAccessExceptionCompat {
         init(/* hasAvailableCapabilities = */ true);
 
         final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
 
-        assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(1);
+        assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(
+                CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
     }
 
     @Config(maxSdk = 32)
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
index d725f7f..9f45cd9 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
@@ -155,6 +155,17 @@
     }
 
     @Test
+    public void setTorchStrengthLevel_throwExceptionWhenNoFlashUnit() throws InterruptedException {
+        Throwable cause = null;
+        try {
+            mNoFlashUnitTorchControl.setTorchStrengthLevel(1).get();
+        } catch (ExecutionException e) {
+            cause = e.getCause();
+        }
+        assertThat(cause).isInstanceOf(UnsupportedOperationException.class);
+    }
+
+    @Test
     public void enableTorch_whenInactive() throws InterruptedException {
         mTorchControl.setActive(false);
         ListenableFuture<Void> listenableFuture = mTorchControl.enableTorch(true);
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 5c0933e..893b313 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -49,7 +49,7 @@
     method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
     method public default int getLensFacing();
     method public default androidx.lifecycle.LiveData<java.lang.Integer!> getLowLightBoostState();
-    method @IntRange(from=1) public default int getMaxTorchStrengthLevel();
+    method @IntRange(from=0) public default int getMaxTorchStrengthLevel();
     method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
     method public int getSensorRotationDegrees();
     method public int getSensorRotationDegrees(int);
@@ -64,6 +64,7 @@
     method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
     method public static boolean mustPlayShutterSound();
     method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+    field public static final int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0; // 0x0
   }
 
   public final class CameraInfoUnavailableException extends java.lang.Exception {
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 5c0933e..893b313 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -49,7 +49,7 @@
     method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
     method public default int getLensFacing();
     method public default androidx.lifecycle.LiveData<java.lang.Integer!> getLowLightBoostState();
-    method @IntRange(from=1) public default int getMaxTorchStrengthLevel();
+    method @IntRange(from=0) public default int getMaxTorchStrengthLevel();
     method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
     method public int getSensorRotationDegrees();
     method public int getSensorRotationDegrees(int);
@@ -64,6 +64,7 @@
     method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
     method public static boolean mustPlayShutterSound();
     method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+    field public static final int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0; // 0x0
   }
 
   public final class CameraInfoUnavailableException extends java.lang.Exception {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
index ac335d1..2332ad6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
@@ -245,6 +245,10 @@
      * {@link ListenableFuture} will fail with an {@link IllegalArgumentException} and it won't
      * modify the torch strength.
      *
+     * <p>If the device doesn't have a flash unit or doesn't support configuring torch strength
+     * level, the returned {@link ListenableFuture} will fail with an
+     * {@link UnsupportedOperationException}.
+     *
      * @param torchStrengthLevel The desired torch strength level.
      * @return a {@link ListenableFuture} that is completed when the torch strength has been
      * applied.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 52a744f..cd2f8a2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -50,6 +50,12 @@
 public interface CameraInfo {
 
     /**
+     * The torch strength level when the device doesn't have a flash unit or doesn't support
+     * adjusting torch strength.
+     */
+    int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0;
+
+    /**
      * An unknown intrinsic zoom ratio. Usually to indicate the camera is unable to provide
      * necessary information to resolve its intrinsic zoom ratio.
      *
@@ -429,12 +435,12 @@
     /**
      * Returns the maximum torch strength level.
      *
-     * @return The maximum strength level, or {code 1} if the device doesn't have a flash unit or
-     * doesn't support configuring torch strength.
+     * @return The maximum strength level, or {@link #TORCH_STRENGTH_LEVEL_UNSUPPORTED} if the
+     * device doesn't have a flash unit or doesn't support configuring torch strength.
      */
-    @IntRange(from = 1)
+    @IntRange(from = 0)
     default int getMaxTorchStrengthLevel() {
-        return 1;
+        return TORCH_STRENGTH_LEVEL_UNSUPPORTED;
     }
 
     /**
@@ -442,9 +448,12 @@
      *
      * <p>The value of the {@link LiveData} will be the default torch strength level of this
      * device if {@link CameraControl#setTorchStrengthLevelAsync(int)} hasn't been called.
+     *
+     * <p>The value of the {@link LiveData} will be {@link #TORCH_STRENGTH_LEVEL_UNSUPPORTED} if
+     * the device doesn't have a flash unit or doesn't support configuring torch strength.
      */
     default @NonNull LiveData<Integer> getTorchStrengthLevel() {
-        return new MutableLiveData<>(1);
+        return new MutableLiveData<>(TORCH_STRENGTH_LEVEL_UNSUPPORTED);
     }
 
     @StringDef(open = true, value = {IMPLEMENTATION_TYPE_UNKNOWN,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index 10d896a..de7943c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -19,6 +19,7 @@
 import android.util.Range;
 import android.util.Size;
 
+import androidx.annotation.IntRange;
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraState;
@@ -78,6 +79,7 @@
     }
 
     @Override
+    @IntRange(from = 0)
     public int getMaxTorchStrengthLevel() {
         return mCameraInfoInternal.getMaxTorchStrengthLevel();
     }
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index befe1c0..c3aa6d1 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -365,13 +365,13 @@
     fun getResolutionInfo_shouldMatchRecordedVideoResolution() {
         // Arrange.
         checkAndBindUseCases(preview, videoCapture)
+        val resolutionInfo = videoCapture.resolutionInfo!!
 
         // Act.
         val result = recordingSession.createRecording().recordAndVerify()
 
         // Assert: the resolution of the video file should match the resolution calculated by
         // rotating the cropRect specified in the ResolutionInfo.
-        val resolutionInfo = videoCapture.resolutionInfo!!
         val expectedResolution =
             rotateSize(rectToSize(resolutionInfo.cropRect), resolutionInfo.rotationDegrees)
         verifyVideoResolution(context, result.file, expectedResolution)
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 19331ab..de52e38 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -1322,7 +1322,7 @@
         Bundle bundle = this.getIntent().getExtras();
         if (bundle != null) {
             mTargetAspectRatio = bundle.getInt(INTENT_EXTRA_TARGET_ASPECT_RATIO,
-                    AspectRatio.RATIO_4_3);
+                    AspectRatio.RATIO_DEFAULT);
             int scaleType = bundle.getInt(INTENT_EXTRA_SCALE_TYPE, INTENT_EXTRA_FILL_CENTER);
             if (scaleType == INTENT_EXTRA_FIT_CENTER) {
                 // Scale the view according to the target aspect ratio, display size and device
diff --git a/car/app/app/src/main/java/androidx/car/app/CarToast.java b/car/app/app/src/main/java/androidx/car/app/CarToast.java
index 0df1034..97bc2a4b 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarToast.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarToast.java
@@ -72,6 +72,7 @@
     /**
      * Creates and sets the text and duration for the toast view.
      *
+     * @param carContext the context used for string localization
      * @param textResId the resource id for the text to show. If the {@code textResId} is 0, the
      *                  text will be set to empty
      * @param duration  how long to display the message. Either {@link #LENGTH_SHORT} or {@link
@@ -89,6 +90,7 @@
     /**
      * Creates and sets the text and duration for the toast view.
      *
+     * @param carContext the CarContext providing the service used to show the toast
      * @param text     the text to show
      * @param duration how long to display the message. Either {@link #LENGTH_SHORT} or {@link
      *                 #LENGTH_LONG}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/AlertCallbackDelegate.java b/car/app/app/src/main/java/androidx/car/app/model/AlertCallbackDelegate.java
index b104443..73f61f3 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/AlertCallbackDelegate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/AlertCallbackDelegate.java
@@ -34,6 +34,7 @@
     /**
      * Notifies that a cancel event happened with given {@code reason}.
      *
+     * @param reason the {@link AlertCallback.Reason} for which the alert was cancelled
      * @param callback the {@link OnDoneCallback} to trigger when the client finishes handling
      *                 the event
      */
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index 78ac040..7f8b8f7 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.text.input
 
+import android.R
 import android.os.Build
 import android.text.InputType
 import android.text.SpannableStringBuilder
@@ -40,7 +41,10 @@
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.foundation.text.computeSizeForDefaultText
 import androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList
+import androidx.compose.foundation.text.input.internal.TextLayoutState
+import androidx.compose.foundation.text.input.internal.TransformedTextFieldState
 import androidx.compose.foundation.text.input.internal.selection.FakeClipboard
+import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
 import androidx.compose.foundation.text.input.internal.setComposingRegion
 import androidx.compose.foundation.text.selection.fetchTextLayoutResult
 import androidx.compose.foundation.verticalScroll
@@ -127,6 +131,9 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
 
 @OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
 @LargeTest
@@ -1130,9 +1137,7 @@
 
         requestFocus(Tag)
 
-        inputMethodInterceptor.withInputConnection {
-            performContextMenuAction(android.R.id.selectAll)
-        }
+        inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.selectAll) }
 
         rule.runOnIdle {
             assertThat(state.selection).isEqualTo(TextRange(0, 5))
@@ -1152,7 +1157,7 @@
 
         requestFocus(Tag)
 
-        inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.cut) }
+        inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.cut) }
 
         rule.waitForIdle()
         assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
@@ -1171,7 +1176,7 @@
 
         requestFocus(Tag)
 
-        inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.copy) }
+        inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.copy) }
 
         rule.waitForIdle()
         assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
@@ -1189,7 +1194,7 @@
 
         requestFocus(Tag)
 
-        inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.paste) }
+        inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.paste) }
 
         rule.runOnIdle {
             assertThat(state.text.toString()).isEqualTo("Worldo")
@@ -1299,6 +1304,34 @@
     }
 
     @Test
+    fun textField_state_invokesAutofill() {
+        val mockLambda: () -> Unit = mock()
+        var density by mutableStateOf(Density(1f))
+
+        val manager =
+            TextFieldSelectionState(
+                    // other parameters not necessary to test autofill invocation
+                    textFieldState =
+                        TransformedTextFieldState(
+                            textFieldState = TextFieldState(),
+                            inputTransformation = null,
+                            codepointTransformation = null,
+                            outputTransformation = null
+                        ),
+                    textLayoutState = TextLayoutState(),
+                    density = density,
+                    enabled = true,
+                    readOnly = false,
+                    isFocused = false,
+                    isPassword = false
+                )
+                .apply { requestAutofillAction = mockLambda }
+
+        manager.autofill()
+        verify(mockLambda, times(1)).invoke()
+    }
+
+    @Test
     fun changingInputTransformation_doesNotRestartInput() {
         var inputTransformation by mutableStateOf(InputTransformation.maxLength(10))
         inputMethodInterceptor.setTextFieldTestContent {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
index a90d80a..50eb946 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
 import androidx.compose.foundation.text.input.internal.selection.FakeClipboard
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
@@ -854,6 +855,38 @@
         }
     }
 
+    @Test
+    fun textField_keyEvent_functionReference() {
+        val state = mutableIntStateOf(0)
+        var handled = -1
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            val stateValue = state.value
+
+            @Suppress("UNUSED_PARAMETER")
+            fun handle(key: KeyEvent): Boolean {
+                handled = stateValue
+                return true
+            }
+
+            BasicTextField(
+                value = "text",
+                onValueChange = {},
+                modifier = Modifier.focusRequester(focusRequester).testTag(tag).onKeyEvent(::handle)
+            )
+        }
+
+        rule.runOnIdle { focusRequester.requestFocus() }
+        rule.onNodeWithTag(tag).performKeyInput { pressKey(Key.A) }
+        rule.runOnIdle {
+            assertThat(handled).isEqualTo(0)
+            state.value += 1
+        }
+
+        rule.onNodeWithTag(tag).performKeyInput { pressKey(Key.A) }
+        rule.runOnIdle { assertThat(handled).isEqualTo(1) }
+    }
+
     private inner class SequenceScope(
         val state: TextFieldState,
         val clipboard: Clipboard,
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index fb28556..9d8e33e 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -23,7 +23,6 @@
 import androidx.compose.foundation.text.LegacyTextFieldState
 import androidx.compose.foundation.text.TextDelegate
 import androidx.compose.foundation.text.TextLayoutResultProxy
-import androidx.compose.ui.autofill.AutofillManager
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -98,7 +97,6 @@
     private val hapticFeedback = mock<HapticFeedback>()
     private val focusRequester = mock<FocusRequester>()
     private val multiParagraph = mock<MultiParagraph>()
-    private val autofillManager = mock<AutofillManager>()
 
     @Before
     fun setup() {
@@ -110,7 +108,6 @@
         manager.textToolbar = textToolbar
         manager.hapticFeedBack = hapticFeedback
         manager.focusRequester = focusRequester
-        manager.autofillManager = autofillManager
         manager.coroutineScope = null
 
         whenever(layoutResult.layoutInput)
@@ -370,10 +367,12 @@
     @Test
     fun autofill_selection_collapse() {
         manager.value = TextFieldValue(text = text, selection = TextRange(4, 4))
+        val mockLambda: () -> Unit = mock()
+        val manager = TextFieldSelectionManager().apply { requestAutofillAction = mockLambda }
 
         manager.autofill()
 
-        verify(autofillManager, times(1)).requestAutofillForActiveElement()
+        verify(mockLambda, times(1)).invoke()
         assertThat(state.handleState).isEqualTo(HandleState.None)
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index 925ecfd..7e63192 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -68,7 +68,6 @@
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.input.pointer.pointerHoverIcon
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalAutofillManager
 import androidx.compose.ui.platform.LocalClipboard
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalHapticFeedback
@@ -301,7 +300,6 @@
     val currentHapticFeedback = LocalHapticFeedback.current
     val currentClipboard = LocalClipboard.current
     val currentTextToolbar = LocalTextToolbar.current
-    val autofillManager = LocalAutofillManager.current
 
     val textToolbarHandler =
         remember(coroutineScope, currentTextToolbar) {
@@ -360,7 +358,6 @@
             enabled = enabled,
             readOnly = readOnly,
             isPassword = isPassword,
-            autofillManager = autofillManager,
             showTextToolbar = textToolbarHandler
         )
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 14f8402..494f392 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -27,6 +27,7 @@
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.foundation.text.handwriting.stylusHandwriting
+import androidx.compose.foundation.text.input.internal.CoreTextFieldSemanticsModifier
 import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
 import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -58,7 +59,6 @@
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.ContentDataType
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.focus.FocusRequester
@@ -82,7 +82,6 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalAutofillManager
 import androidx.compose.ui.platform.LocalClipboard
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalFocusManager
@@ -92,33 +91,13 @@
 import androidx.compose.ui.platform.LocalTextToolbar
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.platform.SoftwareKeyboardController
-import androidx.compose.ui.semantics.contentDataType
-import androidx.compose.ui.semantics.copyText
-import androidx.compose.ui.semantics.cutText
-import androidx.compose.ui.semantics.disabled
-import androidx.compose.ui.semantics.editableText
-import androidx.compose.ui.semantics.getTextLayoutResult
-import androidx.compose.ui.semantics.insertTextAtCursor
-import androidx.compose.ui.semantics.isEditable
-import androidx.compose.ui.semantics.onAutofillText
-import androidx.compose.ui.semantics.onClick
-import androidx.compose.ui.semantics.onImeAction
-import androidx.compose.ui.semantics.onLongClick
-import androidx.compose.ui.semantics.password
-import androidx.compose.ui.semantics.pasteText
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.setSelection
-import androidx.compose.ui.semantics.setText
-import androidx.compose.ui.semantics.textSelectionRange
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.input.CommitTextCommand
-import androidx.compose.ui.text.input.DeleteAllCommand
 import androidx.compose.ui.text.input.EditProcessor
-import androidx.compose.ui.text.input.FinishComposingTextCommand
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.ImeOptions
 import androidx.compose.ui.text.input.KeyboardType
@@ -316,7 +295,6 @@
     manager.coroutineScope = coroutineScope
     manager.textToolbar = LocalTextToolbar.current
     manager.hapticFeedBack = LocalHapticFeedback.current
-    manager.autofillManager = LocalAutofillManager.current
     manager.focusRequester = focusRequester
     manager.editable = !readOnly
     manager.enabled = enabled
@@ -474,151 +452,18 @@
 
     val isPassword = visualTransformation is PasswordVisualTransformation
     val semanticsModifier =
-        Modifier.semantics(true) {
-            // focused semantics are handled by Modifier.focusable()
-            this.editableText = transformedText.text
-            this.textSelectionRange = value.selection
-
-            // The developer will set `contentType`. CTF populates the other autofill-related
-            // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
-            this.contentDataType = ContentDataType.Text
-            onAutofillText { text ->
-                state.justAutofilled = true
-                state.autofillHighlightOn = true
-                handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
-                true
-            }
-
-            if (!enabled) this.disabled()
-            if (isPassword) this.password()
-            val editable = enabled && !readOnly
-            isEditable = editable
-            getTextLayoutResult {
-                if (state.layoutResult != null) {
-                    it.add(state.layoutResult!!.value)
-                    true
-                } else {
-                    false
-                }
-            }
-            if (editable) {
-                setText { text ->
-                    handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
-                    true
-                }
-
-                insertTextAtCursor { text ->
-                    if (readOnly || !enabled) return@insertTextAtCursor false
-
-                    // If the action is performed while in an active text editing session, treat
-                    // this like an IME command and update the text by going through the buffer.
-                    // This keeps the buffer state consistent if other IME commands are performed
-                    // before the next recomposition, and is used for the testing code path.
-                    state.inputSession?.let { session ->
-                        TextFieldDelegate.onEditCommand(
-                            // Finish composing text first because when the field is focused the IME
-                            // might
-                            // set composition.
-                            ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
-                            editProcessor = state.processor,
-                            state.onValueChange,
-                            session
-                        )
-                    }
-                        ?: run {
-                            val newText =
-                                value.text.replaceRange(
-                                    value.selection.start,
-                                    value.selection.end,
-                                    text
-                                )
-                            val newCursor = TextRange(value.selection.start + text.length)
-                            state.onValueChange(TextFieldValue(newText, newCursor))
-                        }
-                    true
-                }
-            }
-
-            setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
-                // in traversal mode we get selection from the `textSelectionRange` semantics which
-                // is
-                // selection in original text. In non-traversal mode selection comes from the
-                // Talkback
-                // and indices are relative to the transformed text
-                val start =
-                    if (relativeToOriginalText) {
-                        selectionStart
-                    } else {
-                        offsetMapping.transformedToOriginal(selectionStart)
-                    }
-                val end =
-                    if (relativeToOriginalText) {
-                        selectionEnd
-                    } else {
-                        offsetMapping.transformedToOriginal(selectionEnd)
-                    }
-
-                if (!enabled) {
-                    false
-                } else if (start == value.selection.start && end == value.selection.end) {
-                    false
-                } else if (
-                    minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
-                ) {
-                    // Do not show toolbar if it's a traversal mode (with the volume keys), or
-                    // if the cursor just moved to beginning or end.
-                    if (relativeToOriginalText || start == end) {
-                        manager.exitSelectionMode()
-                    } else {
-                        manager.enterSelectionMode()
-                    }
-                    state.onValueChange(
-                        TextFieldValue(value.annotatedString, TextRange(start, end))
-                    )
-                    true
-                } else {
-                    manager.exitSelectionMode()
-                    false
-                }
-            }
-            onImeAction(imeOptions.imeAction) {
-                // This will perform the appropriate default action if no handler has been
-                // specified, so
-                // as far as the platform is concerned, we always handle the action and never want
-                // to
-                // defer to the default _platform_ implementation.
-                state.onImeActionPerformed(imeOptions.imeAction)
-                true
-            }
-            onClick {
-                // according to the documentation, we still need to provide proper semantics actions
-                // even if the state is 'disabled'
-                tapToFocus(state, focusRequester, !readOnly)
-                true
-            }
-            onLongClick {
-                manager.enterSelectionMode()
-                true
-            }
-            if (!value.selection.collapsed && !isPassword) {
-                copyText {
-                    manager.copy()
-                    true
-                }
-                if (enabled && !readOnly) {
-                    cutText {
-                        manager.cut()
-                        true
-                    }
-                }
-            }
-            if (enabled && !readOnly) {
-                pasteText {
-                    manager.paste()
-                    true
-                }
-            }
-        }
+        CoreTextFieldSemanticsModifier(
+            transformedText,
+            value,
+            state,
+            readOnly,
+            enabled,
+            isPassword,
+            offsetMapping,
+            manager,
+            imeOptions,
+            focusRequester
+        )
 
     val showCursor = enabled && !readOnly && windowInfo.isWindowFocused && !state.hasHighlight()
     val cursorModifier = Modifier.cursor(state, value, offsetMapping, cursorBrush, showCursor)
@@ -884,30 +729,6 @@
     }
 }
 
-/**
- * In an active input session, semantics updates are handled just as user updates coming from the
- * IME. Otherwise the updates are directly applied on the current state.
- */
-private fun handleTextUpdateFromSemantics(
-    state: LegacyTextFieldState,
-    text: String,
-    readOnly: Boolean,
-    enabled: Boolean
-) {
-    if (readOnly || !enabled) return
-
-    // If the action is performed while in an active text editing session, treat this
-    // like an IME command and update the text by going through the buffer.
-    state.inputSession?.let { session ->
-        TextFieldDelegate.onEditCommand(
-            ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
-            editProcessor = state.processor,
-            state.onValueChange,
-            session
-        )
-    } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
-}
-
 internal class LegacyTextFieldState(
     var textDelegate: TextDelegate,
     val recomposeScope: RecomposeScope,
@@ -1108,7 +929,7 @@
 }
 
 /** Request focus on tap. If already focused, makes sure the keyboard is requested. */
-private fun tapToFocus(
+internal fun tapToFocus(
     state: LegacyTextFieldState,
     focusRequester: FocusRequester,
     allowKeyboard: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt
new file mode 100644
index 0000000..a5fe111
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.foundation.text.input.internal
+
+import androidx.compose.foundation.text.LegacyTextFieldState
+import androidx.compose.foundation.text.TextFieldDelegate
+import androidx.compose.foundation.text.selection.TextFieldSelectionManager
+import androidx.compose.foundation.text.tapToFocus
+import androidx.compose.ui.autofill.ContentDataType
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateSemantics
+import androidx.compose.ui.node.requestAutofill
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.contentDataType
+import androidx.compose.ui.semantics.copyText
+import androidx.compose.ui.semantics.cutText
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.editableText
+import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.insertTextAtCursor
+import androidx.compose.ui.semantics.isEditable
+import androidx.compose.ui.semantics.onAutofillText
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.onImeAction
+import androidx.compose.ui.semantics.onLongClick
+import androidx.compose.ui.semantics.password
+import androidx.compose.ui.semantics.pasteText
+import androidx.compose.ui.semantics.setSelection
+import androidx.compose.ui.semantics.setText
+import androidx.compose.ui.semantics.textSelectionRange
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.CommitTextCommand
+import androidx.compose.ui.text.input.DeleteAllCommand
+import androidx.compose.ui.text.input.FinishComposingTextCommand
+import androidx.compose.ui.text.input.ImeOptions
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TransformedText
+
+internal data class CoreTextFieldSemanticsModifier(
+    val transformedText: TransformedText,
+    val value: TextFieldValue,
+    val state: LegacyTextFieldState,
+    val readOnly: Boolean,
+    val enabled: Boolean,
+    val isPassword: Boolean,
+    val offsetMapping: OffsetMapping,
+    val manager: TextFieldSelectionManager,
+    val imeOptions: ImeOptions,
+    val focusRequester: FocusRequester
+) : ModifierNodeElement<CoreTextFieldSemanticsModifierNode>() {
+    override fun create(): CoreTextFieldSemanticsModifierNode =
+        CoreTextFieldSemanticsModifierNode(
+            transformedText = transformedText,
+            value = value,
+            state = state,
+            readOnly = readOnly,
+            enabled = enabled,
+            isPassword = isPassword,
+            offsetMapping = offsetMapping,
+            manager = manager,
+            imeOptions = imeOptions,
+            focusRequester = focusRequester
+        )
+
+    override fun update(node: CoreTextFieldSemanticsModifierNode) {
+        node.updateNodeSemantics(
+            transformedText = transformedText,
+            value = value,
+            state = state,
+            readOnly = readOnly,
+            enabled = enabled,
+            isPassword = isPassword,
+            offsetMapping = offsetMapping,
+            manager = manager,
+            imeOptions = imeOptions,
+            focusRequester = focusRequester
+        )
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        // Show nothing in the inspector.
+    }
+}
+
+internal class CoreTextFieldSemanticsModifierNode(
+    var transformedText: TransformedText,
+    var value: TextFieldValue,
+    var state: LegacyTextFieldState,
+    var readOnly: Boolean,
+    var enabled: Boolean,
+    var isPassword: Boolean,
+    var offsetMapping: OffsetMapping,
+    var manager: TextFieldSelectionManager,
+    var imeOptions: ImeOptions,
+    var focusRequester: FocusRequester
+) : DelegatingNode(), SemanticsModifierNode {
+    init {
+        manager.requestAutofillAction = { requestAutofill() }
+    }
+
+    override val shouldMergeDescendantSemantics: Boolean
+        get() = true
+
+    override fun SemanticsPropertyReceiver.applySemantics() {
+        this.editableText = transformedText.text
+        this.textSelectionRange = value.selection
+
+        // The developer will set `contentType`. CTF populates the other autofill-related
+        // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
+        this.contentDataType = ContentDataType.Text
+        onAutofillText { text ->
+            state.justAutofilled = true
+            state.autofillHighlightOn = true
+            handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
+            true
+        }
+
+        if (!enabled) this.disabled()
+        if (isPassword) this.password()
+        val editable = enabled && !readOnly
+        isEditable = editable
+        getTextLayoutResult {
+            if (state.layoutResult != null) {
+                it.add(state.layoutResult!!.value)
+                true
+            } else {
+                false
+            }
+        }
+
+        if (editable) {
+            setText { text ->
+                handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
+                true
+            }
+
+            insertTextAtCursor { text ->
+                if (readOnly || !enabled) return@insertTextAtCursor false
+
+                // If the action is performed while in an active text editing session, treat
+                // this like an IME command and update the text by going through the buffer.
+                // This keeps the buffer state consistent if other IME commands are performed
+                // before the next recomposition, and is used for the testing code path.
+                state.inputSession?.let { session ->
+                    TextFieldDelegate.onEditCommand(
+                        // Finish composing text first because when the field is focused the IME
+                        // might
+                        // set composition.
+                        ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
+                        editProcessor = state.processor,
+                        state.onValueChange,
+                        session
+                    )
+                }
+                    ?: run {
+                        val newText =
+                            value.text.replaceRange(
+                                value.selection.start,
+                                value.selection.end,
+                                text
+                            )
+                        val newCursor = TextRange(value.selection.start + text.length)
+                        state.onValueChange(TextFieldValue(newText, newCursor))
+                    }
+                true
+            }
+        }
+
+        setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
+            // in traversal mode we get selection from the `textSelectionRange` semantics which
+            // is selection in original text. In non-traversal mode selection comes from the
+            // Talkback and indices are relative to the transformed text
+            val start =
+                if (relativeToOriginalText) {
+                    selectionStart
+                } else {
+                    offsetMapping.transformedToOriginal(selectionStart)
+                }
+            val end =
+                if (relativeToOriginalText) {
+                    selectionEnd
+                } else {
+                    offsetMapping.transformedToOriginal(selectionEnd)
+                }
+
+            if (!enabled) {
+                false
+            } else if (start == value.selection.start && end == value.selection.end) {
+                false
+            } else if (
+                minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
+            ) {
+                // Do not show toolbar if it's a traversal mode (with the volume keys), or
+                // if the cursor just moved to beginning or end.
+                if (relativeToOriginalText || start == end) {
+                    manager.exitSelectionMode()
+                } else {
+                    manager.enterSelectionMode()
+                }
+                state.onValueChange(TextFieldValue(value.annotatedString, TextRange(start, end)))
+                true
+            } else {
+                manager.exitSelectionMode()
+                false
+            }
+        }
+        onImeAction(imeOptions.imeAction) {
+            // This will perform the appropriate default action if no handler has been
+            // specified, so
+            // as far as the platform is concerned, we always handle the action and never want
+            // to
+            // defer to the default _platform_ implementation.
+            state.onImeActionPerformed(imeOptions.imeAction)
+            true
+        }
+        onClick {
+            // according to the documentation, we still need to provide proper semantics actions
+            // even if the state is 'disabled'
+            tapToFocus(state, focusRequester, !readOnly)
+            true
+        }
+        onLongClick {
+            manager.enterSelectionMode()
+            true
+        }
+        if (!value.selection.collapsed && !isPassword) {
+            copyText {
+                manager.copy()
+                true
+            }
+            if (enabled && !readOnly) {
+                cutText {
+                    manager.cut()
+                    true
+                }
+            }
+        }
+        if (enabled && !readOnly) {
+            pasteText {
+                manager.paste()
+                true
+            }
+        }
+    }
+
+    fun updateNodeSemantics(
+        transformedText: TransformedText,
+        value: TextFieldValue,
+        state: LegacyTextFieldState,
+        readOnly: Boolean,
+        enabled: Boolean,
+        isPassword: Boolean,
+        offsetMapping: OffsetMapping,
+        manager: TextFieldSelectionManager,
+        imeOptions: ImeOptions,
+        focusRequester: FocusRequester
+    ) {
+        // Find the diff: current previous and new values before updating current.
+        val previousEditable = this.enabled && !this.readOnly
+        val previousEnabled = this.enabled
+        val previousIsPassword = this.isPassword
+        val previousImeOptions = this.imeOptions
+        val previousManager = this.manager
+        val editable = enabled && !readOnly
+
+        // Apply the diff.
+        this.transformedText = transformedText
+        this.value = value
+        this.state = state
+        this.readOnly = readOnly
+        this.enabled = enabled
+        this.offsetMapping = offsetMapping
+        this.manager = manager
+        this.imeOptions = imeOptions
+        this.focusRequester = focusRequester
+
+        if (
+            enabled != previousEnabled ||
+                editable != previousEditable ||
+                imeOptions != previousImeOptions ||
+                isPassword != previousIsPassword ||
+                !value.selection.collapsed
+        ) {
+            invalidateSemantics()
+        }
+
+        if (manager != previousManager) {
+            manager.requestAutofillAction = { requestAutofill() }
+        }
+    }
+
+    /**
+     * In an active input session, semantics updates are handled just as user updates coming from
+     * the IME. Otherwise the updates are directly applied on the current state.
+     */
+    private fun handleTextUpdateFromSemantics(
+        state: LegacyTextFieldState,
+        text: String,
+        readOnly: Boolean,
+        enabled: Boolean
+    ) {
+        if (readOnly || !enabled) return
+
+        // If the action is performed while in an active text editing session, treat this
+        // like an IME command and update the text by going through the buffer.
+        state.inputSession?.let { session ->
+            TextFieldDelegate.onEditCommand(
+                ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
+                editProcessor = state.processor,
+                state.onValueChange,
+                session
+            )
+        } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index c9eb589..d7810f9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -66,6 +66,7 @@
 import androidx.compose.ui.node.currentValueOf
 import androidx.compose.ui.node.invalidateSemantics
 import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requestAutofill
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -202,8 +203,9 @@
     ObserverModifierNode,
     LayoutAwareModifierNode {
 
-    private val editable
-        get() = enabled && !readOnly
+    init {
+        textFieldSelectionState.requestAutofillAction = { requestAutofill() }
+    }
 
     private val pointerInputNode =
         delegate(
@@ -430,9 +432,7 @@
         stylusHandwritingTrigger: MutableSharedFlow<Unit>?
     ) {
         // Find the diff: current previous and new values before updating current.
-        val previousEditable = this.editable
-        val editable = enabled && !readOnly
-
+        val previousEditable = this.enabled && !this.readOnly
         val previousEnabled = this.enabled
         val previousTextFieldState = this.textFieldState
         val previousKeyboardOptions = this.keyboardOptions
@@ -440,6 +440,7 @@
         val previousInteractionSource = this.interactionSource
         val previousIsPassword = this.isPassword
         val previousStylusHandwritingTrigger = this.stylusHandwritingTrigger
+        val editable = enabled && !readOnly
 
         // Apply the diff.
         this.textFieldState = textFieldState
@@ -487,6 +488,7 @@
                 textFieldSelectionState.receiveContentConfiguration =
                     receiveContentConfigurationProvider
             }
+            textFieldSelectionState.requestAutofillAction = { requestAutofill() }
         }
 
         if (interactionSource != previousInteractionSource) {
@@ -507,7 +509,8 @@
         if (!enabled) disabled()
         if (isPassword) password()
 
-        isEditable = [email protected]
+        val editable = enabled && !readOnly
+        isEditable = editable
 
         // The developer will set `contentType`. TF populates the other autofill-related
         // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
@@ -630,6 +633,7 @@
         isElementFocused = focusState.isFocused
         onFocusChange()
 
+        val editable = enabled && !readOnly
         if (focusState.isFocused) {
             // Deselect when losing focus even if readonly.
             if (editable) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
index 5baa1a8..0a6c258 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
@@ -66,7 +66,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.autofill.AutofillManager
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.isSpecified
@@ -115,9 +114,6 @@
     var isFocused: Boolean,
     private var isPassword: Boolean,
 ) {
-    /** [AutofillManager] to perform Autofill. */
-    private var autofillManager: AutofillManager? = null
-
     /** [HapticFeedback] handle to perform haptic feedback. */
     private var hapticFeedBack: HapticFeedback? = null
 
@@ -130,6 +126,9 @@
     /** Whether user is interacting with the UI in touch mode. */
     var isInTouchMode: Boolean by mutableStateOf(true)
 
+    /** The action to invoke when autofill is requested in text toolbar. */
+    var requestAutofillAction: (() -> Unit)? = null
+
     /**
      * Reduced [ReceiveContentConfiguration] from the attached modifier node hierarchy. This value
      * is set by [TextFieldDecoratorModifierNode].
@@ -353,8 +352,7 @@
         density: Density,
         enabled: Boolean,
         readOnly: Boolean,
-        isPassword: Boolean,
-        autofillManager: AutofillManager?
+        isPassword: Boolean
     ) {
         if (!enabled) {
             hideTextToolbar()
@@ -366,7 +364,6 @@
         this.enabled = enabled
         this.readOnly = readOnly
         this.isPassword = isPassword
-        this.autofillManager = autofillManager
     }
 
     /** Implements the complete set of gestures supported by the cursor handle. */
@@ -443,7 +440,6 @@
 
         clipboard = null
         hapticFeedBack = null
-        autofillManager = null
     }
 
     /**
@@ -1395,7 +1391,7 @@
      * Inserts credentials (if there exist any that match this field type) into the text field.
      */
     fun autofill() {
-        autofillManager?.requestAutofillForActiveElement()
+        requestAutofillAction?.invoke()
     }
 
     fun deselect() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
index 363a279..a47838d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -62,6 +62,8 @@
     autoSize: TextAutoSize? = null,
     private var onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)? = null
 ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode {
+    override val shouldAutoInvalidate: Boolean
+        get() = false
 
     private val textAnnotatedStringNode =
         delegate(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index 8a5c009..e8543a8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -82,6 +82,9 @@
     private var autoSize: TextAutoSize? = null,
     private var onShowTranslation: ((TextSubstitutionValue) -> Unit)? = null
 ) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
+    override val shouldAutoInvalidate: Boolean
+        get() = false
+
     @Suppress("PrimitiveInCollection")
     private var baselineCache: MutableMap<AlignmentLine, Int>? = null
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
index a8dd10b..e561553 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
@@ -77,6 +77,9 @@
     private var minLines: Int = DefaultMinLines,
     private var overrideColor: ColorProducer? = null
 ) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
+    override val shouldAutoInvalidate: Boolean
+        get() = false
+
     @Suppress("PrimitiveInCollection") // Map required for use in public API.
     // Usages of this collection are so few that the gains of using
     // MutableObjectIntMap<AlignmentLine> and then converting to a Map<AlignmentLine, Int>
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 674b87a..6f75364 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -38,7 +38,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillManager
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -100,8 +99,8 @@
      */
     internal var visualTransformation: VisualTransformation = VisualTransformation.None
 
-    /** [AutofillManager] to perform clipboard features. */
-    internal var autofillManager: AutofillManager? = null
+    /** The action to invoke when autofill is requested in text toolbar. */
+    internal var requestAutofillAction: (() -> Unit)? = null
 
     /** [Clipboard] to perform clipboard features. */
     internal var clipboard: Clipboard? = null
@@ -705,7 +704,7 @@
     }
 
     internal fun autofill() {
-        autofillManager?.requestAutofillForActiveElement()
+        requestAutofillAction?.invoke()
     }
 
     internal fun getHandlePosition(isStartHandle: Boolean): Offset {
diff --git a/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt
new file mode 100644
index 0000000..828090b
--- /dev/null
+++ b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.material.ripple
+
+import android.graphics.RenderNode
+import android.os.Build
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Test for [RippleHostView] */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class RippleHostViewTest {
+
+    @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+    /**
+     * Test for b/377222399
+     *
+     * Note, without the corresponding fix this test would only fail on Samsung devices, unless
+     * manually changing RippleDrawable.mRippleStyle.mRippleStyle to STYLE_SOLID through reflection.
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun doesNotDrawWhileUnattached() {
+        rule.runOnUiThread {
+            val activity = rule.activity
+
+            // View is explicitly not attached
+            val rippleHostView = RippleHostView(activity)
+
+            // Add a ripple while unattached
+            rippleHostView.addRipple(
+                PressInteraction.Press(Offset.Zero),
+                true,
+                Size(100f, 100f),
+                radius = 10,
+                color = Color.Red,
+                alpha = 0.4f,
+                onInvalidateRipple = {}
+            )
+
+            // Create a hardware backed canvas
+            val canvas = RenderNode("RippleHostViewTest").beginRecording()
+
+            // Should not crash
+            rippleHostView.draw(canvas)
+        }
+    }
+}
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
index d1f3bf4..ef949d5 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.res.ColorStateList
+import android.graphics.Canvas
 import android.graphics.Rect
 import android.graphics.drawable.ColorDrawable
 import android.graphics.drawable.Drawable
@@ -52,6 +53,15 @@
         // noop
     }
 
+    override fun draw(canvas: Canvas) {
+        if (!isAttachedToWindow) {
+            // Cleanup any existing ripples if we added a ripple after being detached b/377222399
+            disposeRipple()
+            return
+        }
+        super.draw(canvas)
+    }
+
     override fun refreshDrawableState() {
         // We don't want the View to manage the drawable state, so avoid updating the ripple's
         // state (via View.mBackground) when we lose window focus, or other events.
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
index 68e6215..d2c14a0 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
@@ -18,11 +18,13 @@
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertTextEquals
@@ -112,6 +114,25 @@
     }
 
     @Test
+    fun testChangingFontSizeDoesNotInvalidateSemantics() {
+        val fontSize = mutableStateOf(16.sp)
+        var count = 0
+        val countModifier = Modifier.semantics { count++ }
+        rule.setContent {
+            ProvideTextStyle(ExpectedTextStyle) {
+                Box(Modifier.background(Color.White)) {
+                    Text(modifier = countModifier, text = TestText, fontSize = fontSize.value)
+                }
+            }
+        }
+        rule.runOnIdle {
+            count = 0
+            fontSize.value = 20.sp
+        }
+        rule.runOnIdle { assertThat(count).isEqualTo(0) }
+    }
+
+    @Test
     fun settingCustomTextStyle() {
         var textColor: Color? = null
         var textAlign: TextAlign? = null
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.ignore b/compose/material3/adaptive/adaptive-layout/api/current.ignore
new file mode 100644
index 0000000..6562a08
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ParameterNameChange: androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem#ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole, T) parameter #1:
+    Attempted to change parameter name from content to contentKey in constructor androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index d075899..b4cd5c9 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -94,7 +94,7 @@
     property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
+  @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
     field public static final androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion Companion;
   }
 
@@ -110,12 +110,28 @@
     property @androidx.compose.runtime.Composable public abstract String description;
   }
 
-  public static final class PaneExpansionAnchor.Offset extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
-    ctor public PaneExpansionAnchor.Offset(float offset);
-    method @androidx.compose.runtime.Composable public String getDescription();
-    method public float getOffset();
-    property @androidx.compose.runtime.Composable public String description;
+  public abstract static class PaneExpansionAnchor.Offset extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
+    method public final int getDirection();
+    method public final float getOffset();
+    property public final int direction;
     property public final float offset;
+    field public static final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset.Companion Companion;
+  }
+
+  public static final class PaneExpansionAnchor.Offset.Companion {
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset fromEnd(float offset);
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset fromStart(float offset);
+  }
+
+  @kotlin.jvm.JvmInline public static final value class PaneExpansionAnchor.Offset.Direction {
+    field public static final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset.Direction.Companion Companion;
+  }
+
+  public static final class PaneExpansionAnchor.Offset.Direction.Companion {
+    method public int getFromEnd();
+    method public int getFromStart();
+    property public final int FromEnd;
+    property public final int FromStart;
   }
 
   public static final class PaneExpansionAnchor.Proportion extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
@@ -277,8 +293,8 @@
   }
 
   public final class PaneScaffoldDirectiveKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+    method public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+    method public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldMotionDataProvider<Role> {
@@ -335,7 +351,7 @@
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState? paneExpansionState);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldRole {
+  public final class SupportingPaneScaffoldRole {
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getExtra();
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getMain();
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getSupporting();
@@ -360,7 +376,7 @@
     method public operator androidx.compose.material3.adaptive.layout.AdaptStrategy get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldDestinationItem<T> {
+  public final class ThreePaneScaffoldDestinationItem<T> {
     ctor public ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
     method public T? getContentKey();
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getPane();
@@ -431,7 +447,7 @@
     property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
+  @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
     ctor public ThreePaneScaffoldValue(String primary, String secondary, String tertiary);
     method public operator String get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
     method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getPaneExpansionStateKey();
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.ignore b/compose/material3/adaptive/adaptive-layout/api/restricted_current.ignore
new file mode 100644
index 0000000..6562a08
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ParameterNameChange: androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem#ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole, T) parameter #1:
+    Attempted to change parameter name from content to contentKey in constructor androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index d075899..b4cd5c9 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -94,7 +94,7 @@
     property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
+  @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
     field public static final androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion Companion;
   }
 
@@ -110,12 +110,28 @@
     property @androidx.compose.runtime.Composable public abstract String description;
   }
 
-  public static final class PaneExpansionAnchor.Offset extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
-    ctor public PaneExpansionAnchor.Offset(float offset);
-    method @androidx.compose.runtime.Composable public String getDescription();
-    method public float getOffset();
-    property @androidx.compose.runtime.Composable public String description;
+  public abstract static class PaneExpansionAnchor.Offset extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
+    method public final int getDirection();
+    method public final float getOffset();
+    property public final int direction;
     property public final float offset;
+    field public static final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset.Companion Companion;
+  }
+
+  public static final class PaneExpansionAnchor.Offset.Companion {
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset fromEnd(float offset);
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset fromStart(float offset);
+  }
+
+  @kotlin.jvm.JvmInline public static final value class PaneExpansionAnchor.Offset.Direction {
+    field public static final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset.Direction.Companion Companion;
+  }
+
+  public static final class PaneExpansionAnchor.Offset.Direction.Companion {
+    method public int getFromEnd();
+    method public int getFromStart();
+    property public final int FromEnd;
+    property public final int FromStart;
   }
 
   public static final class PaneExpansionAnchor.Proportion extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
@@ -277,8 +293,8 @@
   }
 
   public final class PaneScaffoldDirectiveKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+    method public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+    method public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldMotionDataProvider<Role> {
@@ -335,7 +351,7 @@
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState? paneExpansionState);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldRole {
+  public final class SupportingPaneScaffoldRole {
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getExtra();
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getMain();
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getSupporting();
@@ -360,7 +376,7 @@
     method public operator androidx.compose.material3.adaptive.layout.AdaptStrategy get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldDestinationItem<T> {
+  public final class ThreePaneScaffoldDestinationItem<T> {
     ctor public ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
     method public T? getContentKey();
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getPane();
@@ -431,7 +447,7 @@
     property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
+  @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
     ctor public ThreePaneScaffoldValue(String primary, String secondary, String tertiary);
     method public operator String get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
     method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getPaneExpansionStateKey();
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt
index 681cae4..344c0f6 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt
@@ -233,7 +233,7 @@
                         ThreePaneScaffoldRole.Secondary,
                         ThreePaneScaffoldRole.Tertiary
                     ),
-                    PaneExpansionStateData(7, 0.8F, 9, PaneExpansionAnchor.Offset(200.dp))
+                    PaneExpansionStateData(7, 0.8F, 9, PaneExpansionAnchor.Offset.fromStart(200.dp))
                 ),
                 Pair(
                     TwoPaneExpansionStateKeyImpl(
@@ -271,12 +271,12 @@
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val MockAnchor0 = PaneExpansionAnchor.Proportion(0f)
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private val MockAnchor1 = PaneExpansionAnchor.Offset(200.dp)
+private val MockAnchor1 = PaneExpansionAnchor.Offset.fromStart(200.dp)
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val MockAnchor2 = PaneExpansionAnchor.Proportion(0.5f)
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private val MockAnchor3 = PaneExpansionAnchor.Offset((-200).dp)
+private val MockAnchor3 = PaneExpansionAnchor.Offset.fromEnd(200.dp)
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val MockAnchor4 = PaneExpansionAnchor.Proportion(1f)
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private val MockAnchor5 = PaneExpansionAnchor.Offset(500.dp)
+private val MockAnchor5 = PaneExpansionAnchor.Offset.fromStart(500.dp)
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
index a7fd150..3677863 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
@@ -303,7 +303,7 @@
                     anchors =
                         listOf(
                             PaneExpansionAnchor.Proportion(0f),
-                            PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor)
+                            PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor)
                         )
                 )
             mockDraggingPx = with(LocalDensity.current) { 200.dp.toPx() }
@@ -340,7 +340,7 @@
         rule.runOnIdle {
             scope.launch {
                 mockPaneExpansionState.animateTo(
-                    PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor)
+                    PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor)
                 )
             }
         }
@@ -368,7 +368,7 @@
         rule.runOnIdle {
             scope.launch {
                 mockPaneExpansionState.animateTo(
-                    PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor),
+                    PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor),
                     200F
                 )
             }
@@ -394,7 +394,7 @@
         rule.runOnIdle {
             scope.launch {
                 assertFailsWith<IllegalArgumentException> {
-                    mockPaneExpansionState.animateTo(PaneExpansionAnchor.Offset(10.dp))
+                    mockPaneExpansionState.animateTo(PaneExpansionAnchor.Offset.fromStart(10.dp))
                 }
             }
         }
@@ -411,7 +411,7 @@
 private val MockPaneExpansionAnchors =
     listOf(
         PaneExpansionAnchor.Proportion(0f),
-        PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor),
+        PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor),
         PaneExpansionAnchor.Proportion(1f),
     )
 
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.android.kt b/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.android.kt
index a563fe9..5bab1c8 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.android.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.android.kt
@@ -59,7 +59,12 @@
             get() =
                 Strings(R.string.m3_adaptive_default_pane_expansion_proportion_anchor_description)
 
-        actual inline val defaultPaneExpansionOffsetAnchorDescription
-            get() = Strings(R.string.m3_adaptive_default_pane_expansion_offset_anchor_description)
+        actual inline val defaultPaneExpansionStartOffsetAnchorDescription
+            get() =
+                Strings(R.string.m3_adaptive_default_pane_expansion_start_offset_anchor_description)
+
+        actual inline val defaultPaneExpansionEndOffsetAnchorDescription
+            get() =
+                Strings(R.string.m3_adaptive_default_pane_expansion_end_offset_anchor_description)
     }
 }
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-as/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-as/strings.xml
index d550f15..120b08a 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-as/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-as/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"পে’ন সম্প্ৰসাৰণ কৰিবলৈ টনা হেণ্ডেল"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"পে’নৰ বিভাজন %sলৈ সলনি কৰক"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d শতাংশ"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DPs"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rCA/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rCA/strings.xml
index 33e0970..c1e08b3 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rCA/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rCA/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Pane expansion drag handle"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Change pane split to %s"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d percent"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DPs"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hi/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hi/strings.xml
index d25df6b..f333cf2 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hi/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hi/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"पैनल को बड़ा करने के लिए, खींचकर छोड़ने वाला हैंडल"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"पैनल स्प्लिट को %s में बदलें"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d प्रतिशत"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d डीपी"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ja/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ja/strings.xml
index c287ba9..bfbceb0 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ja/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ja/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"ペインの展開のドラッグ ハンドル"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"ペインの分割を %s に変更"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d パーセント"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DP"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ka/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ka/strings.xml
index d5cbb1b..f9f4e65 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ka/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ka/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"არეს გაფართოების სახელური ჩავლებისთვის"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"არეს გაყოფის შეცვლა %s-ით"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d პროცენტი"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DPs"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ml/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ml/strings.xml
index 0747fcb..2b63179c 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ml/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ml/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"പെയിൻ വികസിപ്പിക്കാനായി വലിച്ചിടുന്നതിനുള്ള ഹാൻഡിൽ"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"പെയിൻ വിഭജനം %s ആയി മാറ്റുക"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d ശതമാനം"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DP-കൾ"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ms/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ms/strings.xml
index 81f4fb8..889db66e 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ms/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ms/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Pemegang seret pengembangan anak tetingkap"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Tukar anak tetingkap terpisah kepada %s"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d peratus"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DP"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pl/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pl/strings.xml
index 19a0cd0..ec85843 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pl/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pl/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Uchwyt do przeciągania panelu"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Zmień podział panelu na %s"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d procent"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DP"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rPT/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rPT/strings.xml
index 2dae797..115f3c0 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rPT/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rPT/strings.xml
@@ -20,5 +20,4 @@
     <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Indicador para arrastar de expansão do painel"</string>
     <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Altere a divisão do painel para %s"</string>
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d por cento"</string>
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DPs"</string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values/strings.xml
index 38b807f..94e3ffa 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values/strings.xml
@@ -28,7 +28,14 @@
     <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description">
         %d percent
     </string>
-    <!-- Spoken description of a pane expansion anchor point based on offset in DPs a user can
-         anchor the pane expansion to. -->
-    <string name="m3_adaptive_default_pane_expansion_offset_anchor_description">%d DPs</string>
+    <!-- Spoken description of a pane expansion anchor point based on offset from start in DPs a
+         user can anchor the pane expansion to. -->
+    <string name="m3_adaptive_default_pane_expansion_start_offset_anchor_description">
+        %d DPs from start
+    </string>
+    <!-- Spoken description of a pane expansion anchor point based on offset from end in DPs a user
+         can anchor the pane expansion to. -->
+    <string name="m3_adaptive_default_pane_expansion_end_offset_anchor_description">
+        %d DPs from end
+    </string>
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneAdaptedValue.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneAdaptedValue.kt
index 00deed0..080903b 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneAdaptedValue.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneAdaptedValue.kt
@@ -16,13 +16,10 @@
 
 package androidx.compose.material3.adaptive.layout
 
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-
 /**
  * The adapted state of a pane. It gives clues to pane scaffolds about if a certain pane should be
  * composed and how.
  */
-@ExperimentalMaterial3AdaptiveApi
 @JvmInline
 value class PaneAdaptedValue private constructor(private val description: String) {
     companion object {
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
index 226fc29..733dc1f 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
@@ -370,13 +370,10 @@
      * @param initialVelocity the initial velocity of the animation
      */
     suspend fun animateTo(anchor: PaneExpansionAnchor, initialVelocity: Float = 0F) {
-        require(Snapshot.withoutReadObservation { anchors.contains(anchor) }) {
-            "The provided $anchor is not in the anchor list!"
-        }
+        require(anchors.contains(anchor)) { "The provided $anchor is not in the anchor list!" }
         currentAnchor = anchor
         measuredDensity?.apply {
-            val position =
-                Snapshot.withoutReadObservation { anchor.positionIn(maxExpansionWidth, this) }
+            val position = anchor.positionIn(maxExpansionWidth, this)
             animateToInternal(position, initialVelocity)
         }
     }
@@ -405,11 +402,11 @@
                     anchors.toPositions(
                         // When maxExpansionWidth is updated, the anchor positions will be
                         // recalculated.
-                        Snapshot.withoutReadObservation { maxExpansionWidth },
+                        maxExpansionWidth,
                         it
                     )
             }
-            if (!anchors.contains(Snapshot.withoutReadObservation { currentAnchor })) {
+            if (!anchors.contains(currentAnchor)) {
                 currentAnchor = null
             }
             this.anchoringAnimationSpec = anchoringAnimationSpec
@@ -591,7 +588,10 @@
      * [PaneExpansionAnchor] implementation that specifies the anchor position in the proportion of
      * the total size of the layout at the start side of the anchor.
      *
-     * @property proportion the proportion of the layout at the start side of the anchor. layout.
+     * @param proportion the proportion of the layout at the start side of the anchor. For example,
+     *   if the current layout from the start to the end is list-detail, when the proportion value
+     *   is 0.3 and this anchor is used, the list pane will occupy 30% of the layout and the detail
+     *   pane will occupy 70% of it.
      */
     class Proportion(@FloatRange(0.0, 1.0) val proportion: Float) : PaneExpansionAnchor() {
         override val type = ProportionType
@@ -619,40 +619,107 @@
     }
 
     /**
-     * [PaneExpansionAnchor] implementation that specifies the anchor position in the offset in
-     * [Dp]. If a positive value is provided, the offset will be treated as a start offset, on the
-     * other hand, if a negative value is provided, the absolute value of the provided offset will
-     * be used as an end offset. For example, if -150.dp is provided, the resulted anchor will be at
-     * the position that is 150dp away from the end side of the associated layout.
+     * [PaneExpansionAnchor] implementation that specifies the anchor position based on the offset
+     * in [Dp].
      *
      * @property offset the offset of the anchor in [Dp].
      */
-    class Offset(val offset: Dp) : PaneExpansionAnchor() {
-        override val type = OffsetType
-
-        override val description
-            @Composable
-            get() =
-                getString(Strings.defaultPaneExpansionOffsetAnchorDescription, offset.value.toInt())
-
-        override fun positionIn(totalSizePx: Int, density: Density) =
-            with(density) { offset.roundToPx() }.let { if (it < 0) totalSizePx + it else it }
+    abstract class Offset internal constructor(val offset: Dp, override internal val type: Int) :
+        PaneExpansionAnchor() {
+        /**
+         * Indicates the direction of the offset.
+         *
+         * @see Direction.FromStart
+         * @see Direction.FromEnd
+         */
+        val direction: Direction = Direction(type)
 
         override fun equals(other: Any?): Boolean {
             if (this === other) return true
             if (other !is Offset) return false
-            return offset == other.offset
+            return offset == other.offset && direction == other.direction
         }
 
         override fun hashCode(): Int {
-            return offset.hashCode()
+            return offset.hashCode() * 31 + direction.hashCode()
+        }
+
+        /** Represents the direction from where the offset will be calculated. */
+        @JvmInline
+        value class Direction internal constructor(internal val value: Int) {
+            companion object {
+                /**
+                 * Indicates the offset will be calculated from the start. For example, if the
+                 * offset is 150.dp, the resulted anchor will be at the position that is 150dp away
+                 * from the start side of the associated layout.
+                 */
+                val FromStart = Direction(OffsetFromStartType)
+
+                /**
+                 * Indicates the offset will be calculated from the end. For example, if the offset
+                 * is 150.dp, the resulted anchor will be at the position that is 150dp away from
+                 * the end side of the associated layout.
+                 */
+                val FromEnd = Direction(OffsetFromEndType)
+            }
+        }
+
+        private class StartOffset(offset: Dp) : Offset(offset, OffsetFromStartType) {
+            override val description
+                @Composable
+                get() =
+                    getString(
+                        Strings.defaultPaneExpansionStartOffsetAnchorDescription,
+                        offset.value.toInt()
+                    )
+
+            override fun positionIn(totalSizePx: Int, density: Density) =
+                with(density) { offset.roundToPx() }
+        }
+
+        private class EndOffset(offset: Dp) : Offset(offset, OffsetFromEndType) {
+            override val description
+                @Composable
+                get() =
+                    getString(
+                        Strings.defaultPaneExpansionEndOffsetAnchorDescription,
+                        offset.value.toInt()
+                    )
+
+            override fun positionIn(totalSizePx: Int, density: Density) =
+                totalSizePx - with(density) { offset.roundToPx() }
+        }
+
+        companion object {
+            /**
+             * Create an [androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset]
+             * anchor from the start side of the layout.
+             *
+             * @param offset offset to be used in [Dp].
+             */
+            fun fromStart(offset: Dp): Offset {
+                require(offset >= 0.dp) { "Offset must larger than or equal to 0 dp." }
+                return StartOffset(offset)
+            }
+
+            /**
+             * Create an [androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset]
+             * anchor from the end side of the layout.
+             *
+             * @param offset offset to be used in [Dp].
+             */
+            fun fromEnd(offset: Dp): Offset {
+                require(offset >= 0.dp) { "Offset must larger than or equal to 0 dp." }
+                return EndOffset(offset)
+            }
         }
     }
 
     internal companion object {
         internal const val UnspecifiedType = 0
         internal const val ProportionType = 1
-        internal const val OffsetType = 2
+        internal const val OffsetFromStartType = 2
+        internal const val OffsetFromEndType = 3
     }
 }
 
@@ -728,8 +795,10 @@
                 when (currentAnchorType) {
                     PaneExpansionAnchor.ProportionType ->
                         PaneExpansionAnchor.Proportion(it[6] as Float)
-                    PaneExpansionAnchor.OffsetType ->
-                        PaneExpansionAnchor.Offset((it[6] as Float).dp)
+                    PaneExpansionAnchor.OffsetFromStartType ->
+                        PaneExpansionAnchor.Offset.fromStart((it[6] as Float).dp)
+                    PaneExpansionAnchor.OffsetFromEndType ->
+                        PaneExpansionAnchor.Offset.fromEnd((it[6] as Float).dp)
                     else -> null
                 }
             object : Map.Entry<PaneExpansionStateKey, PaneExpansionStateData> {
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
index a2a06c7..8c32cd7 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
@@ -18,7 +18,6 @@
 
 package androidx.compose.material3.adaptive.layout
 
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.Posture
 import androidx.compose.material3.adaptive.WindowAdaptiveInfo
 import androidx.compose.material3.adaptive.allVerticalHingeBounds
@@ -44,7 +43,6 @@
  *   vertical hinges.
  * @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
  */
-@ExperimentalMaterial3AdaptiveApi
 @Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
 fun calculatePaneScaffoldDirective(
     windowAdaptiveInfo: WindowAdaptiveInfo,
@@ -69,7 +67,6 @@
     val maxVerticalPartitions: Int
     val verticalPartitionSpacerSize: Dp
 
-    // TODO(conradchen): Confirm the table top mode settings
     if (windowAdaptiveInfo.windowPosture.isTabletop) {
         maxVerticalPartitions = 2
         verticalPartitionSpacerSize = 24.dp
@@ -109,7 +106,6 @@
  *   vertical hinges.
  * @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
  */
-@ExperimentalMaterial3AdaptiveApi
 @Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
 fun calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
     windowAdaptiveInfo: WindowAdaptiveInfo,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.kt
index 4ff2966..e11af66 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.kt
@@ -27,7 +27,8 @@
         val defaultPaneExpansionDragHandleContentDescription: Strings
         val defaultPaneExpansionDragHandleActionDescription: Strings
         val defaultPaneExpansionProportionAnchorDescription: Strings
-        val defaultPaneExpansionOffsetAnchorDescription: Strings
+        val defaultPaneExpansionStartOffsetAnchorDescription: Strings
+        val defaultPaneExpansionEndOffsetAnchorDescription: Strings
     }
 }
 
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
index 1fae8c8..2764c13 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
@@ -177,7 +177,6 @@
  * three pane scaffolds. We suggest you to use the values defined here instead of the raw
  * [ThreePaneScaffoldRole] under the context of [SupportingPaneScaffold] for better code clarity.
  */
-@ExperimentalMaterial3AdaptiveApi
 object SupportingPaneScaffoldRole {
     /**
      * The main pane of [SupportingPaneScaffold], which is supposed to hold the major content of an
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
index 8ebafe4..fc1dfd9 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
@@ -16,8 +16,6 @@
 
 package androidx.compose.material3.adaptive.layout
 
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-
 /**
  * An item representing a navigation destination in a [ThreePaneScaffold].
  *
@@ -25,7 +23,6 @@
  * @param contentKey the optional key or id representing the content of the destination. The type
  *   [T] must be storable in a Bundle.
  */
-@ExperimentalMaterial3AdaptiveApi
 class ThreePaneScaffoldDestinationItem<out T>(
     val pane: ThreePaneScaffoldRole,
     val contentKey: T? = null,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
index d119518..71c42c2 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
@@ -22,7 +22,6 @@
 import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.ui.util.fastForEachReversed
 
-@ExperimentalMaterial3AdaptiveApi
 private inline fun buildThreePaneScaffoldValue(
     buildAction: (ThreePaneScaffoldRole) -> PaneAdaptedValue
 ): ThreePaneScaffoldValue {
@@ -168,7 +167,7 @@
  * @param tertiary [PaneAdaptedValue] of the tertiary pane of [ThreePaneScaffold]
  * @constructor create an instance of [ThreePaneScaffoldValue]
  */
-@ExperimentalMaterial3AdaptiveApi
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
 @Immutable
 class ThreePaneScaffoldValue(
     val primary: PaneAdaptedValue,
diff --git a/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.jvmStubs.kt b/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.jvmStubs.kt
index a419136..11184b6 100644
--- a/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.jvmStubs.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.jvmStubs.kt
@@ -39,7 +39,9 @@
             implementedInJetBrainsFork()
         actual val defaultPaneExpansionProportionAnchorDescription: Strings =
             implementedInJetBrainsFork()
-        actual val defaultPaneExpansionOffsetAnchorDescription: Strings =
+        actual val defaultPaneExpansionStartOffsetAnchorDescription: Strings =
+            implementedInJetBrainsFork()
+        actual val defaultPaneExpansionEndOffsetAnchorDescription: Strings =
             implementedInJetBrainsFork()
     }
 }
diff --git a/compose/material3/adaptive/adaptive-navigation/api/current.txt b/compose/material3/adaptive/adaptive-navigation/api/current.txt
index 0e485ac..ba0712b 100644
--- a/compose/material3/adaptive/adaptive-navigation/api/current.txt
+++ b/compose/material3/adaptive/adaptive-navigation/api/current.txt
@@ -6,7 +6,7 @@
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> void NavigableSupportingPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState? paneExpansionState);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
+  @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
     field public static final androidx.compose.material3.adaptive.navigation.BackNavigationBehavior.Companion Companion;
   }
 
diff --git a/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt b/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
index 0e485ac..ba0712b 100644
--- a/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
@@ -6,7 +6,7 @@
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> void NavigableSupportingPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState? paneExpansionState);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
+  @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
     field public static final androidx.compose.material3.adaptive.navigation.BackNavigationBehavior.Companion Companion;
   }
 
diff --git a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
index 66f554d7..37c8d7f 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
@@ -16,13 +16,11 @@
 
 package androidx.compose.material3.adaptive.navigation
 
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
 import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
 import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
 
 /** A class to control how back navigation should behave in a [ThreePaneScaffoldNavigator]. */
-@ExperimentalMaterial3AdaptiveApi
 @JvmInline
 value class BackNavigationBehavior private constructor(private val description: String) {
     override fun toString(): String = this.description
diff --git a/compose/material3/adaptive/adaptive/api/current.txt b/compose/material3/adaptive/adaptive/api/current.txt
index 7dc7be8..a93216b 100644
--- a/compose/material3/adaptive/adaptive/api/current.txt
+++ b/compose/material3/adaptive/adaptive/api/current.txt
@@ -2,7 +2,7 @@
 package androidx.compose.material3.adaptive {
 
   public final class AndroidPosture_androidKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
+    method public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
   }
 
   public final class AndroidWindowAdaptiveInfo_androidKt {
diff --git a/compose/material3/adaptive/adaptive/api/restricted_current.txt b/compose/material3/adaptive/adaptive/api/restricted_current.txt
index 7dc7be8..a93216b 100644
--- a/compose/material3/adaptive/adaptive/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive/api/restricted_current.txt
@@ -2,7 +2,7 @@
 package androidx.compose.material3.adaptive {
 
   public final class AndroidPosture_androidKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
+    method public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
   }
 
   public final class AndroidWindowAdaptiveInfo_androidKt {
diff --git a/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt b/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
index 60b16e60..1ace339 100644
--- a/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
+++ b/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
@@ -23,7 +23,6 @@
  * Calculates the [Posture] for a given list of [FoldingFeature]s. This methods converts framework
  * folding info into the Material-opinionated posture info.
  */
-@ExperimentalMaterial3AdaptiveApi
 fun calculatePosture(foldingFeatures: List<FoldingFeature>): Posture {
     var isTableTop = false
     val hingeList = mutableListOf<HingeInfo>()
diff --git a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
index 798e57e..5181bf1 100644
--- a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
+++ b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
@@ -438,8 +438,8 @@
 private val PaneExpansionAnchors =
     listOf(
         PaneExpansionAnchor.Proportion(0f),
-        PaneExpansionAnchor.Proportion(0.25f),
+        PaneExpansionAnchor.Offset.fromStart(360.dp),
         PaneExpansionAnchor.Proportion(0.5f),
-        PaneExpansionAnchor.Proportion(0.75f),
+        PaneExpansionAnchor.Offset.fromEnd(360.dp),
         PaneExpansionAnchor.Proportion(1f),
     )
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 97ef2d4..273f29f 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -272,25 +272,25 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ButtonGroupDefaults {
-    method public float getAnimateFraction();
-    method public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonPressShape();
-    method public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonPressShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonShape();
     method public float getConnectedSpaceBetween();
-    method public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonPressShape();
-    method public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonShape();
-    method public float getSpaceBetween();
-    property public final float animateFraction;
-    property public final androidx.compose.ui.graphics.Shape connectedLeadingButtonPressShape;
-    property public final androidx.compose.ui.graphics.Shape connectedLeadingButtonShape;
-    property public final float connectedSpaceBetween;
-    property public final androidx.compose.ui.graphics.Shape connectedTrailingButtonPressShape;
-    property public final androidx.compose.ui.graphics.Shape connectedTrailingButtonShape;
-    property public final float spaceBetween;
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonPressShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonShape();
+    method public float getExpandedRatio();
+    method public androidx.compose.foundation.layout.Arrangement.Horizontal getHorizontalArrangement();
+    property public final float ConnectedSpaceBetween;
+    property public final float ExpandedRatio;
+    property public final androidx.compose.foundation.layout.Arrangement.Horizontal HorizontalArrangement;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedLeadingButtonPressShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedLeadingButtonShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedTrailingButtonPressShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedTrailingButtonShape;
     field public static final androidx.compose.material3.ButtonGroupDefaults INSTANCE;
   }
 
   public final class ButtonGroupKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ButtonGroup(optional androidx.compose.ui.Modifier modifier, optional @FloatRange(from=0.0) float animateFraction, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ButtonGroupScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ButtonGroup(optional androidx.compose.ui.Modifier modifier, optional @FloatRange(from=0.0) float expandedRatio, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ButtonGroupScope,kotlin.Unit> content);
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public interface ButtonGroupScope {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 97ef2d4..273f29f 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -272,25 +272,25 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ButtonGroupDefaults {
-    method public float getAnimateFraction();
-    method public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonPressShape();
-    method public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonPressShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonShape();
     method public float getConnectedSpaceBetween();
-    method public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonPressShape();
-    method public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonShape();
-    method public float getSpaceBetween();
-    property public final float animateFraction;
-    property public final androidx.compose.ui.graphics.Shape connectedLeadingButtonPressShape;
-    property public final androidx.compose.ui.graphics.Shape connectedLeadingButtonShape;
-    property public final float connectedSpaceBetween;
-    property public final androidx.compose.ui.graphics.Shape connectedTrailingButtonPressShape;
-    property public final androidx.compose.ui.graphics.Shape connectedTrailingButtonShape;
-    property public final float spaceBetween;
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonPressShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonShape();
+    method public float getExpandedRatio();
+    method public androidx.compose.foundation.layout.Arrangement.Horizontal getHorizontalArrangement();
+    property public final float ConnectedSpaceBetween;
+    property public final float ExpandedRatio;
+    property public final androidx.compose.foundation.layout.Arrangement.Horizontal HorizontalArrangement;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedLeadingButtonPressShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedLeadingButtonShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedTrailingButtonPressShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedTrailingButtonShape;
     field public static final androidx.compose.material3.ButtonGroupDefaults INSTANCE;
   }
 
   public final class ButtonGroupKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ButtonGroup(optional androidx.compose.ui.Modifier modifier, optional @FloatRange(from=0.0) float animateFraction, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ButtonGroupScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ButtonGroup(optional androidx.compose.ui.Modifier modifier, optional @FloatRange(from=0.0) float expandedRatio, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ButtonGroupScope,kotlin.Unit> content);
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public interface ButtonGroupScope {
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonGroupSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonGroupSamples.kt
index cdd3981..aedab65 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonGroupSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonGroupSamples.kt
@@ -107,8 +107,8 @@
 
     ButtonGroup(
         modifier = Modifier.padding(horizontal = 8.dp),
-        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.connectedSpaceBetween),
-        animateFraction = 0f
+        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
+        expandedRatio = 0f
     ) {
         options.forEachIndexed { index, label ->
             ToggleButton(
@@ -158,8 +158,8 @@
 
     ButtonGroup(
         modifier = Modifier.padding(horizontal = 8.dp),
-        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.connectedSpaceBetween),
-        animateFraction = 0f
+        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
+        expandedRatio = 0f
     ) {
         options.forEachIndexed { index, label ->
             ToggleButton(
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonGroupTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonGroupTest.kt
index fb0b3e3..156c847 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonGroupTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonGroupTest.kt
@@ -101,9 +101,9 @@
     @Test
     fun default_firstPressed_buttonSizing() {
         val width = 75.dp
-        val animateFraction = 0.15f
-        val expectedExpandWidth = width + (width * animateFraction)
-        val expectedCompressWidth = width - (width * animateFraction)
+        val expandedWeight = 0.15f
+        val expectedExpandWidth = width + (width * expandedWeight)
+        val expectedCompressWidth = width - (width * expandedWeight)
 
         rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(wrapperTestTag)) {
@@ -147,9 +147,9 @@
     @Test
     fun default_secondPressed_buttonSizing() {
         val width = 75.dp
-        val animateFraction = 0.15f
-        val expectedExpandWidth = width + (width * animateFraction)
-        val expectedCompressWidth = width - (width * (animateFraction / 2f))
+        val expandedWeight = 0.15f
+        val expectedExpandWidth = width + (width * expandedWeight)
+        val expectedCompressWidth = width - (width * (expandedWeight / 2f))
 
         rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(wrapperTestTag)) {
@@ -193,9 +193,9 @@
     @Test
     fun default_thirdPressed_buttonSizing() {
         val width = 75.dp
-        val animateFraction = 0.15f
-        val expectedExpandWidth = width + (width * animateFraction)
-        val expectedCompressWidth = width - (width * (animateFraction / 2f))
+        val expandedWeight = 0.15f
+        val expectedExpandWidth = width + (width * expandedWeight)
+        val expectedCompressWidth = width - (width * (expandedWeight / 2f))
 
         rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(wrapperTestTag)) {
@@ -239,9 +239,9 @@
     @Test
     fun default_fourthPressed_buttonSizing() {
         val width = 75.dp
-        val animateFraction = 0.15f
-        val expectedExpandWidth = width + (width * animateFraction)
-        val expectedCompressWidth = width - (width * animateFraction)
+        val expandedWeight = 0.15f
+        val expectedExpandWidth = width + (width * expandedWeight)
+        val expectedCompressWidth = width - (width * expandedWeight)
 
         rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(wrapperTestTag)) {
@@ -285,13 +285,13 @@
     @Test
     fun customAnimateFraction_firstPressed_buttonSizing() {
         val width = 75.dp
-        val animateFraction = 0.3f
-        val expectedExpandWidth = width + (width * animateFraction)
-        val expectedCompressWidth = width - (width * animateFraction)
+        val expandedWeight = 0.3f
+        val expectedExpandWidth = width + (width * expandedWeight)
+        val expectedCompressWidth = width - (width * expandedWeight)
 
         rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(wrapperTestTag)) {
-                ButtonGroup(animateFraction = animateFraction) {
+                ButtonGroup(expandedRatio = expandedWeight) {
                     Button(modifier = Modifier.width(width).testTag(aButton), onClick = {}) {
                         Text("A")
                     }
@@ -331,13 +331,13 @@
     @Test
     fun customAnimateFraction_secondPressed_buttonSizing() {
         val width = 75.dp
-        val animateFraction = 0.3f
-        val expectedExpandWidth = width + (width * animateFraction)
-        val expectedCompressWidth = width - (width * animateFraction / 2f)
+        val expandedWeight = 0.3f
+        val expectedExpandWidth = width + (width * expandedWeight)
+        val expectedCompressWidth = width - (width * expandedWeight / 2f)
 
         rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(wrapperTestTag)) {
-                ButtonGroup(animateFraction = animateFraction) {
+                ButtonGroup(expandedRatio = expandedWeight) {
                     Button(modifier = Modifier.width(width).testTag(aButton), onClick = {}) {
                         Text("A")
                     }
@@ -377,13 +377,13 @@
     @Test
     fun customAnimateFraction_thirdPressed_buttonSizing() {
         val width = 75.dp
-        val animateFraction = 0.3f
-        val expectedExpandWidth = width + (width * animateFraction)
-        val expectedCompressWidth = width - (width * animateFraction / 2f)
+        val expandedWeight = 0.3f
+        val expectedExpandWidth = width + (width * expandedWeight)
+        val expectedCompressWidth = width - (width * expandedWeight / 2f)
 
         rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(wrapperTestTag)) {
-                ButtonGroup(animateFraction = animateFraction) {
+                ButtonGroup(expandedRatio = expandedWeight) {
                     Button(modifier = Modifier.width(width).testTag(aButton), onClick = {}) {
                         Text("A")
                     }
@@ -423,13 +423,13 @@
     @Test
     fun customAnimateFraction_fourthPressed_buttonSizing() {
         val width = 75.dp
-        val animateFraction = 0.3f
-        val expectedExpandWidth = width + (width * animateFraction)
-        val expectedCompressWidth = width - (width * animateFraction)
+        val expandedWeight = 0.3f
+        val expectedExpandWidth = width + (width * expandedWeight)
+        val expectedCompressWidth = width - (width * expandedWeight)
 
         rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(wrapperTestTag)) {
-                ButtonGroup(animateFraction = animateFraction) {
+                ButtonGroup(expandedRatio = expandedWeight) {
                     Button(modifier = Modifier.width(width).testTag(aButton), onClick = {}) {
                         Text("A")
                     }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
index ace32d4..b555741 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
@@ -47,6 +47,7 @@
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
@@ -80,9 +81,13 @@
  * @sample androidx.compose.material3.samples.MultiSelectConnectedButtonGroupSample
  * @sample androidx.compose.material3.samples.SingleSelectConnectedButtonGroupSample
  * @param modifier the [Modifier] to be applied to the button group.
- * @param animateFraction the percentage, represented by a float, of the width of the interacted
- *   child element that will be used to expand the interacted child element as well as compress the
- *   neighboring children.
+ * @param expandedRatio the percentage, represented by a float, of the width of the interacted child
+ *   element that will be used to expand the interacted child element as well as compress the
+ *   neighboring children. By Default, standard button group will expand the interacted child
+ *   element by [ButtonGroupDefaults.ExpandedRatio] of its width and this will be propagated to its
+ *   neighbors. If 0f is passed into this slot, then the interacted child element will not expand at
+ *   all and the neighboring elements will not compress. If 1f is passed into this slot, then the
+ *   interacted child element will expand to 200% of its default width when pressed.
  * @param horizontalArrangement The horizontal arrangement of the button group's children.
  * @param content the content displayed in the button group, expected to use a Material3 component
  *   or a composable that is tagged with [Modifier.interactionSourceData].
@@ -91,9 +96,8 @@
 @ExperimentalMaterial3ExpressiveApi
 fun ButtonGroup(
     modifier: Modifier = Modifier,
-    @FloatRange(0.0) animateFraction: Float = ButtonGroupDefaults.animateFraction,
-    horizontalArrangement: Arrangement.Horizontal =
-        Arrangement.spacedBy(ButtonGroupDefaults.spaceBetween),
+    @FloatRange(0.0) expandedRatio: Float = ButtonGroupDefaults.ExpandedRatio,
+    horizontalArrangement: Arrangement.Horizontal = ButtonGroupDefaults.HorizontalArrangement,
     content: @Composable ButtonGroupScope.() -> Unit
 ) {
     // TODO Load the motionScheme tokens from the component tokens file
@@ -132,7 +136,7 @@
                                 pressedIndex = index
                                 coroutineScope.launch {
                                     anim.animateTo(
-                                        targetValue = animateFraction,
+                                        targetValue = expandedRatio,
                                         animationSpec = defaultAnimationSpec
                                     )
                                 }
@@ -181,56 +185,66 @@
     /**
      * The default percentage, represented as a float, of the width of the interacted child element
      * that will be used to expand the interacted child element as well as compress the neighboring
-     * children.
+     * children. By Default, standard button group will expand the interacted child element by 15%
+     * of its width and this will be propagated to its neighbors.
      */
-    val animateFraction = 0.15f
+    val ExpandedRatio = 0.15f
 
-    /** The default spacing used between children. */
-    val spaceBetween = ButtonGroupSmallTokens.BetweenSpace
+    /** The default Arrangement used between children. */
+    val HorizontalArrangement: Arrangement.Horizontal =
+        Arrangement.spacedBy(ButtonGroupSmallTokens.BetweenSpace)
 
     /** The default spacing used between children for connected button group */
     // TODO replace with token value
-    val connectedSpaceBetween = 2.dp
+    val ConnectedSpaceBetween: Dp = 2.dp
 
     /** Default shape for the leading button in a connected button group */
-    val connectedLeadingButtonShape: Shape =
-        // TODO replace with token value
-        RoundedCornerShape(
-            topStart = ShapeDefaults.CornerFull,
-            bottomStart = ShapeDefaults.CornerFull,
-            topEnd = ShapeDefaults.CornerSmall,
-            bottomEnd = ShapeDefaults.CornerSmall
-        )
+    val connectedLeadingButtonShape: Shape
+        @Composable
+        get() =
+            // TODO replace with token value
+            RoundedCornerShape(
+                topStart = ShapeDefaults.CornerFull,
+                bottomStart = ShapeDefaults.CornerFull,
+                topEnd = ShapeDefaults.CornerSmall,
+                bottomEnd = ShapeDefaults.CornerSmall
+            )
 
     /** Default shape for the pressed state for the leading button in a connected button group. */
-    val connectedLeadingButtonPressShape: Shape =
-        // TODO replace with token value
-        RoundedCornerShape(
-            topStart = ShapeDefaults.CornerFull,
-            bottomStart = ShapeDefaults.CornerFull,
-            topEnd = ShapeDefaults.CornerExtraSmall,
-            bottomEnd = ShapeDefaults.CornerExtraSmall
-        )
+    val connectedLeadingButtonPressShape: Shape
+        @Composable
+        get() =
+            // TODO replace with token value
+            RoundedCornerShape(
+                topStart = ShapeDefaults.CornerFull,
+                bottomStart = ShapeDefaults.CornerFull,
+                topEnd = ShapeDefaults.CornerExtraSmall,
+                bottomEnd = ShapeDefaults.CornerExtraSmall
+            )
 
     /** Default shape for the trailing button in a connected button group */
-    val connectedTrailingButtonShape: Shape =
-        // TODO replace with token value
-        RoundedCornerShape(
-            topEnd = ShapeDefaults.CornerFull,
-            bottomEnd = ShapeDefaults.CornerFull,
-            topStart = ShapeDefaults.CornerSmall,
-            bottomStart = ShapeDefaults.CornerSmall
-        )
+    val connectedTrailingButtonShape: Shape
+        @Composable
+        get() =
+            // TODO replace with token value
+            RoundedCornerShape(
+                topEnd = ShapeDefaults.CornerFull,
+                bottomEnd = ShapeDefaults.CornerFull,
+                topStart = ShapeDefaults.CornerSmall,
+                bottomStart = ShapeDefaults.CornerSmall
+            )
 
     /** Default shape for the pressed state for the trailing button in a connected button group. */
-    val connectedTrailingButtonPressShape: Shape =
-        // TODO replace with token value
-        RoundedCornerShape(
-            topEnd = ShapeDefaults.CornerFull,
-            bottomEnd = ShapeDefaults.CornerFull,
-            topStart = ShapeDefaults.CornerExtraSmall,
-            bottomStart = ShapeDefaults.CornerExtraSmall
-        )
+    val connectedTrailingButtonPressShape: Shape
+        @Composable
+        get() =
+            // TODO replace with token value
+            RoundedCornerShape(
+                topEnd = ShapeDefaults.CornerFull,
+                bottomEnd = ShapeDefaults.CornerFull,
+                topStart = ShapeDefaults.CornerExtraSmall,
+                bottomStart = ShapeDefaults.CornerExtraSmall
+            )
 }
 
 private class ButtonGroupMeasurePolicy(
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 5a884ad..2b88ca8 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -228,7 +228,6 @@
   public abstract class AutofillManager {
     method public abstract void cancel();
     method public abstract void commit();
-    method public abstract void requestAutofillForActiveElement();
   }
 
   public final class AutofillNode {
@@ -2860,6 +2859,7 @@
 
   public final class DelegatableNodeKt {
     method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+    method public static void requestAutofill(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.graphics.GraphicsContext requireGraphicsContext(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.layout.LayoutCoordinates requireLayoutCoordinates(androidx.compose.ui.node.DelegatableNode);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c0d712..8b114f0 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -228,7 +228,6 @@
   public abstract class AutofillManager {
     method public abstract void cancel();
     method public abstract void commit();
-    method public abstract void requestAutofillForActiveElement();
   }
 
   public final class AutofillNode {
@@ -2914,6 +2913,7 @@
 
   public final class DelegatableNodeKt {
     method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+    method public static void requestAutofill(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.graphics.GraphicsContext requireGraphicsContext(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.layout.LayoutCoordinates requireLayoutCoordinates(androidx.compose.ui.node.DelegatableNode);
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index d6ce34a..25dcd50 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -42,11 +42,14 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.elementOf
+import androidx.compose.ui.node.requestAutofill
 import androidx.compose.ui.platform.LocalAutofillManager
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.contentDataType
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.contentType
@@ -713,68 +716,6 @@
 
     @Test
     @SmallTest
-    @SdkSuppress(minSdkVersion = 26)
-    fun autofillManager_requestAutofillAfterFocus() {
-        val am: PlatformAutofillManager = mock()
-        val contextMenuTag = "menu_tag"
-        var autofillManager: AutofillManager?
-
-        rule.setContent {
-            autofillManager = LocalAutofillManager.current
-            (autofillManager as AndroidAutofillManager).platformAutofillManager = am
-            Box(
-                modifier =
-                    Modifier.semantics {
-                            testTag = contextMenuTag
-                            onAutofillText { true }
-                        }
-                        .focusProperties { canFocus = true }
-                        .clickable { autofillManager?.requestAutofillForActiveElement() }
-                        .size(height, width)
-            )
-        }
-
-        // `requestAutofill` is always called after an element is focused
-        rule.onNodeWithTag(contextMenuTag).requestFocus()
-        rule.runOnIdle { verify(am).notifyViewEntered(any(), any(), any()) }
-
-        // then `requestAutofill` is called on that same previously focused element
-        rule.onNodeWithTag(contextMenuTag).performClick()
-        rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
-    }
-
-    @Test
-    @SmallTest
-    @SdkSuppress(minSdkVersion = 26)
-    fun autofillManager_notAutofillable_doesNotrequestAutofillAfterFocus() {
-        val am: PlatformAutofillManager = mock()
-        val contextMenuTag = "menu_tag"
-        var autofillManager: AutofillManager?
-
-        rule.setContent {
-            autofillManager = LocalAutofillManager.current
-            (autofillManager as AndroidAutofillManager).platformAutofillManager = am
-            Box(
-                modifier =
-                    Modifier.semantics { testTag = contextMenuTag }
-                        .focusProperties { canFocus = true }
-                        .clickable { autofillManager?.requestAutofillForActiveElement() }
-                        .size(height, width)
-            )
-        }
-        clearInvocations(am)
-
-        // `requestAutofill` is always called after an element is focused
-        rule.onNodeWithTag(contextMenuTag).requestFocus()
-        rule.runOnIdle { verifyZeroInteractions(am) }
-
-        // then `requestAutofill` is called on that same previously focused element
-        rule.onNodeWithTag(contextMenuTag).performClick()
-        rule.runOnIdle { verifyNoMoreInteractions(am) }
-    }
-
-    @Test
-    @SmallTest
     fun autofillManager_lazyColumnScroll_callsCommit() {
         lateinit var state: LazyListState
         lateinit var coroutineScope: CoroutineScope
@@ -866,4 +807,35 @@
         // A column disappearing will call commit
         rule.runOnIdle { verify(am).commit() }
     }
+
+    @Test
+    @SmallTest
+    @SdkSuppress(minSdkVersion = 26)
+    fun autofillManager_requestAutofill() {
+        val am: PlatformAutofillManager = mock()
+        val semanticsModifier = TestSemanticsModifier { testTag = "TestTag" }
+        var autofillManager: AutofillManager?
+
+        rule.setContent {
+            autofillManager = LocalAutofillManager.current
+            (autofillManager as AndroidAutofillManager).platformAutofillManager = am
+            Box(Modifier.elementOf(semanticsModifier))
+        }
+
+        // Act
+        rule.runOnIdle { semanticsModifier.requestAutofill() }
+
+        // Assert
+        rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
+    }
+
+    private class TestSemanticsModifier(
+        private val onApplySemantics: SemanticsPropertyReceiver.() -> Unit
+    ) : SemanticsModifierNode, Modifier.Node() {
+
+        override fun SemanticsPropertyReceiver.applySemantics() {
+            contentType = ContentType.Username
+            onApplySemantics.invoke(this)
+        }
+    }
 }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index 84314e3e..1d90f0b 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -3510,6 +3510,10 @@
 
     override fun requestFocus(): Boolean = false
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override fun measureAndLayout(sendPointerUpdate: Boolean) {}
 
     override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 3f9150a..ffb3fec 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -2846,6 +2846,10 @@
 
     override fun requestFocus(): Boolean = false
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override val rootForTest: RootForTest
         get() = TODO("Not yet implemented")
 
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index 76aa7ce..4322a1c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -281,6 +281,10 @@
 
     override fun requestFocus() = TODO("Not yet implemented")
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override fun onSemanticsChange() {}
 
     override fun getFocusDirection(keyEvent: KeyEvent) = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index a36713d..5d5e649 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -521,6 +521,10 @@
 
     override fun requestFocus(): Boolean = false
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override fun measureAndLayout(sendPointerUpdate: Boolean) {}
 
     override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
index 7cfa2d4c..c49374e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
@@ -77,9 +77,7 @@
         // Assert.
         rule.runOnIdle {
             if (precomputedSemantics) {
-                // One invocation when the modifier node calls autoInvalidateNodeSelf and another
-                // when the Layout node is attached.
-                assertThat(semanticsModifier.applySemanticsInvocations).isEqualTo(2)
+                assertThat(semanticsModifier.applySemanticsInvocations).isEqualTo(1)
             } else {
                 assertThat(semanticsModifier.applySemanticsInvocations).isEqualTo(0)
             }
@@ -194,7 +192,7 @@
         // Assert.
         rule.runOnIdle {
             assertThat(semanticsModifier.applySemanticsInvocations)
-                .isEqualTo(if (precomputedSemantics) 8 else 2)
+                .isEqualTo(if (precomputedSemantics) 7 else 2)
         }
     }
 
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 8e4a41b..4c213f7 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -26,12 +26,16 @@
 import androidx.compose.material.Surface
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.RecomposeScope
 import androidx.compose.runtime.ReusableContent
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.currentRecomposeScope
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.autofill.ContentDataType
 import androidx.compose.ui.autofill.ContentType
@@ -134,6 +138,40 @@
             .assert(SemanticsMatcher.expectValue(SemanticsProperties.PaneTitle, paneTitleString))
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun testSemanticsCalculatedOncePerComposition() {
+        var recomposeScope: RecomposeScope? = null
+        var count = 0
+        fun Modifier.count() = semantics { count++ }
+        rule.setContent {
+            recomposeScope = currentRecomposeScope
+            Box(
+                modifier = Modifier.count().count().count(),
+            )
+        }
+        rule.runOnIdle {
+            if (ComposeUiFlags.isSemanticAutofillEnabled) {
+                // with autofill on, semantics is eagerly evaluated
+                assertThat(count).isEqualTo(3)
+            } else {
+                // before autofill, semantics was lazily evaluated
+                assertThat(count).isEqualTo(0)
+            }
+            count = 0
+            recomposeScope!!.invalidate()
+        }
+        rule.runOnIdle {
+            if (ComposeUiFlags.isSemanticAutofillEnabled) {
+                // with autofill on, semantics is eagerly evaluated
+                assertThat(count).isEqualTo(3)
+            } else {
+                // before autofill, semantics was lazily evaluated
+                assertThat(count).isEqualTo(0)
+            }
+        }
+    }
+
     @Test
     @Suppress("DEPRECATION")
     fun isContainerProperty_unmergedConfig() {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 69f31cc..4d49938 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -77,19 +77,6 @@
         platformAutofillManager.cancel()
     }
 
-    // This will be used to request autofill when
-    // `AutofillManager.requestAutofillForActiveElement()` is called (e.g. from the text toolbar).
-    private var previouslyFocusedId = -1
-
-    override fun requestAutofillForActiveElement() {
-        if (previouslyFocusedId <= 0) return
-
-        rectManager.rects.withRect(previouslyFocusedId) { left, top, right, bottom ->
-            reusableRect.set(left, top, right, bottom)
-            platformAutofillManager.requestAutofill(view, previouslyFocusedId, reusableRect)
-        }
-    }
-
     override fun onFocusChanged(
         previous: FocusTargetModifierNode?,
         current: FocusTargetModifierNode?
@@ -105,7 +92,6 @@
                 rectManager.rects.withRect(semanticsId) { l, t, r, b ->
                     platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
                 }
-                previouslyFocusedId = semanticsId
             }
         }
     }
@@ -138,7 +124,6 @@
             val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
             val currFocus = config?.getOrNull(SemanticsProperties.Focused)
             if (previousFocus != true && currFocus == true && config.isAutofillable()) {
-                previouslyFocusedId = semanticsId
                 rectManager.rects.withRect(semanticsId) { l, t, r, b ->
                     platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
                 }
@@ -236,6 +221,13 @@
     private var currentlyDisplayedIDs = MutableIntSet()
     private var pendingChangesToDisplayedIds = false
 
+    internal fun requestAutofill(semanticsInfo: SemanticsInfo) {
+        rectManager.rects.withRect(semanticsInfo.semanticsId) { left, top, right, bottom ->
+            reusableRect.set(left, top, right, bottom)
+            platformAutofillManager.requestAutofill(view, semanticsInfo.semanticsId, reusableRect)
+        }
+    }
+
     internal fun onPostAttach(semanticsInfo: SemanticsInfo) {
         if (semanticsInfo.semanticsConfiguration?.isRelatedToAutoCommit() == true) {
             currentlyDisplayedIDs.add(semanticsInfo.semanticsId)
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 c059f5d..7da948c 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
@@ -1247,6 +1247,13 @@
         }
     }
 
+    override fun requestAutofill(node: LayoutNode) {
+        @OptIn(ExperimentalComposeUiApi::class)
+        if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+            _autofillManager?.requestAutofill(node)
+        }
+    }
+
     fun requestClearInvalidObservations() {
         observationClearRequested = true
     }
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index 0328f65..25e747f 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -776,7 +776,8 @@
     check(owner is MockOwner)
     owner.onRequestMeasureParams.clear()
     owner.invalidatedLayers.clear()
-    owner.semanticsChanged = false
+    layoutNode.isSemanticsInvalidated = false
+    //    owner.semanticsChanged = false
 }
 
 private fun NodeChain.layoutInvalidated(): Boolean {
@@ -792,9 +793,7 @@
 }
 
 private fun NodeChain.semanticsInvalidated(): Boolean {
-    val owner = layoutNode.owner
-    check(owner is MockOwner)
-    return owner.semanticsChanged
+    return layoutNode.isSemanticsInvalidated
 }
 
 internal fun layout(
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index f13b1f1..faa645f 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -2448,6 +2448,10 @@
 
     override fun requestFocus(): Boolean = false
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override fun measureAndLayout(sendPointerUpdate: Boolean) {}
 
     override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
@@ -2538,11 +2542,7 @@
         }
     }
 
-    var semanticsChanged: Boolean = false
-
-    override fun onSemanticsChange() {
-        semanticsChanged = true
-    }
+    override fun onSemanticsChange() {}
 
     override fun onLayoutChange(layoutNode: LayoutNode) {
         layoutChangeCount++
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 4fdd4ea..de1b8d6 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -446,6 +446,10 @@
 
         override fun requestFocus() = TODO("Not yet implemented")
 
+        override fun requestAutofill(node: LayoutNode) {
+            TODO("Not yet implemented")
+        }
+
         override fun measureAndLayout(sendPointerUpdate: Boolean) = TODO("Not yet implemented")
 
         override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
index 2a6163b..f5ebdc7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
@@ -42,14 +42,4 @@
      * without processing any information entered in the autofillable field.
      */
     abstract fun cancel()
-
-    /**
-     * Request autofill for previously focused element.
-     *
-     * This may have no effect, and it is not required that any autofill service will be notified.
-     *
-     * Any component that can be autofilled may call this when it is active to request an autofill
-     * services response.
-     */
-    abstract fun requestAutofillForActiveElement()
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index cba5ff1..d60c1b5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -46,7 +46,7 @@
 fun Modifier.onPreviewKeyEvent(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier =
     this then KeyInputElement(onKeyEvent = null, onPreKeyEvent = onPreviewKeyEvent)
 
-private data class KeyInputElement(
+private class KeyInputElement(
     val onKeyEvent: ((KeyEvent) -> Boolean)?,
     val onPreKeyEvent: ((KeyEvent) -> Boolean)?
 ) : ModifierNodeElement<KeyInputNode>() {
@@ -67,6 +67,21 @@
             properties["onPreviewKeyEvent"] = it
         }
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is KeyInputElement) return false
+
+        if (onKeyEvent !== other.onKeyEvent) return false
+        if (onPreKeyEvent !== other.onPreKeyEvent) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = onKeyEvent?.hashCode() ?: 0
+        result = 31 * result + (onPreKeyEvent?.hashCode() ?: 0)
+        return result
+    }
 }
 
 private class KeyInputNode(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 08505aa..8b45c3f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -332,6 +332,13 @@
     checkPreconditionNotNull(requireLayoutNode().owner) { "This node does not have an owner." }
 
 /**
+ * Requests autofill for the LayoutNode that this [DelegatableNode] is attached to. If the node does
+ * not have any autofill semantic properties set, then the request still may be sent to the Autofill
+ * service, but no response is expected.
+ */
+fun DelegatableNode.requestAutofill() = requireLayoutNode().requestAutofill()
+
+/**
  * Returns the current [Density] of the LayoutNode that this [DelegatableNode] is attached to. If
  * the node is not attached, this function will throw an [IllegalStateException].
  */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 5f60779..ef5efb6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -400,7 +400,15 @@
 
     override fun isTransparent(): Boolean = outerCoordinator.isTransparent()
 
-    private var isSemanticsInvalidated = false
+    internal var isSemanticsInvalidated = false
+
+    internal fun requestAutofill() {
+        // Ignore calls while semantics are being applied (b/378114177).
+        if (isCurrentlyCalculatingSemanticsConfiguration) return
+
+        val owner = requireOwner()
+        owner.requestAutofill(this)
+    }
 
     internal fun invalidateSemantics() {
         // Ignore calls to invalidate Semantics while semantics are being applied (b/378114177).
@@ -923,6 +931,9 @@
             requirePrecondition(!isDeactivated) { "modifier is updated when deactivated" }
             if (isAttached) {
                 applyModifier(value)
+                if (isSemanticsInvalidated) {
+                    invalidateSemantics()
+                }
             } else {
                 pendingModifier = value
             }
@@ -935,19 +946,6 @@
         if (lookaheadRoot == null && nodes.has(Nodes.ApproachMeasure)) {
             lookaheadRoot = this
         }
-        // Notify semantics listeners if semantics was invalidated.
-        @OptIn(ExperimentalComposeUiApi::class)
-        if (ComposeUiFlags.isSemanticAutofillEnabled && isSemanticsInvalidated) {
-            val prev = _semanticsConfiguration
-            _semanticsConfiguration = calculateSemanticsConfiguration()
-            isSemanticsInvalidated = false
-            val owner = requireOwner()
-            owner.semanticsOwner.notifySemanticsChange(this, prev)
-
-            // This is needed for Accessibility and ContentCapture. Remove after these systems
-            // are migrated to use SemanticsInfo and SemanticListeners.
-            owner.onSemanticsChange()
-        }
     }
 
     private fun resetModifierState() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index a4f5fe6..eb27c9a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -331,7 +331,7 @@
         node.invalidateDraw()
     }
     if (Nodes.Semantics in selfKindSet && node is SemanticsModifierNode) {
-        node.invalidateSemantics()
+        node.requireLayoutNode().isSemanticsInvalidated = true
     }
     if (Nodes.ParentData in selfKindSet && node is ParentDataModifierNode) {
         node.invalidateParentData()
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 a75bf87..ee7342a 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
@@ -245,6 +245,9 @@
      */
     fun requestFocus(): Boolean
 
+    /** Ask the system to request autofill values to this owner. */
+    fun requestAutofill(node: LayoutNode)
+
     /**
      * Iterates through all LayoutNodes that have requested layout and measures and lays them out.
      * If [sendPointerUpdate] is `true` then a simulated PointerEvent may be sent to update pointer
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
index 43db9f6..d8d5f55 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
@@ -125,6 +125,7 @@
  * index than startIndex, and at the end of the Carousel, we will not populate the slots that have a
  * higher index than startIndex.
  *
+ * @param motionScene the MotionScene which holds slots and can be used to customize the carousel
  * @param initialSlotIndex the slot index that holds the current element
  * @param numSlots the number of slots in the scene
  * @param backwardTransition the name of the previous transition (default "previous")
@@ -137,7 +138,7 @@
 @Composable
 @Suppress("UnavailableSymbol")
 fun MotionCarousel(
-    @Suppress("HiddenTypeParameter") motionScene: MotionScene,
+    motionScene: MotionScene,
     initialSlotIndex: Int,
     numSlots: Int,
     backwardTransition: String = "backward",
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/ArrayLinkedVariables.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/ArrayLinkedVariables.java
index 3575eb4..fef3c9c 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/ArrayLinkedVariables.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/ArrayLinkedVariables.java
@@ -230,8 +230,10 @@
      * The code is broadly identical to the put() method, only differing
      * in in-line deletion, and of course doing an add rather than a put
      *
-     * @param variable the variable we want to add
-     * @param value    its value
+     * @param variable             the variable we want to add
+     * @param value                its value
+     * @param removeFromDefinition if a variable's value becomes zero, it is removed. This parameter
+     *                             is whether the removal is deep now or left shallow until cleanup
      */
     @Override
     public void add(SolverVariable variable, float value, boolean removeFromDefinition) {
@@ -355,7 +357,9 @@
     /**
      * Update the current list with a new definition
      *
-     * @param definition the row containing the definition
+     * @param definition           the row containing the definition
+     * @param removeFromDefinition if a variable's value becomes zero, it is removed. This parameter
+     *                             is whether the removal is deep now or left shallow until cleanup
      */
     @Override
     public float use(ArrayRow definition, boolean removeFromDefinition) {
@@ -374,7 +378,9 @@
     /**
      * Remove a variable from the list
      *
-     * @param variable the variable we want to remove
+     * @param variable             the variable we want to remove
+     * @param removeFromDefinition if a variable's value becomes zero, it is removed. This parameter
+     *                             is whether the removal is deep now or left shallow until cleanup
      * @return the value of the removed variable
      */
     @Override
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/LinearSystem.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/LinearSystem.java
index 5706151..e4e1c7af 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/LinearSystem.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/LinearSystem.java
@@ -1509,6 +1509,8 @@
      * Add the equations constraining a widget center to another widget center, positioned
      * on a circle, following an angle and radius
      *
+     * @param widget the constrained widget
+     * @param target the constrained-to widget
      * @param angle  from 0 to 360
      * @param radius the distance between the two centers
      */
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
index f2dc0ee..ba98915 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
@@ -280,7 +280,8 @@
      * x coordinate of "time" 0.0 mPoints[point.length-1] is filled with the y coordinate of "time"
      * 1.0
      *
-     * @param points array to fill (should be 2x the number of mPoints
+     * @param points     array to fill (should be 2x pointCount)
+     * @param pointCount truncate mPoints to this length. Must be > 1 and <= mPoints.length
      */
     public void buildPath(float[] points, int pointCount) {
         float mils = 1.0f / (pointCount - 1);
@@ -1646,6 +1647,7 @@
      * ...
      * length
      *
+     * @param type if type is -1, skip all keyframes with type != -1
      * @param info is a data structure array of int that holds info on each keyframe
      * @return Number of keyFrames found
      */
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/KeyCycleOscillator.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/KeyCycleOscillator.java
index 943b41d..b3c5443 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/KeyCycleOscillator.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/KeyCycleOscillator.java
@@ -139,9 +139,12 @@
      * sets a oscillator wave point
      *
      * @param framePosition the position
+     * @param shape         the wave shape
+     * @param waveString    the wave string
      * @param variesBy      only varies by path supported for now
      * @param period        the period of the wave
      * @param offset        the offset value
+     * @param phase         the phase of the new wavepoint
      * @param value         the adder
      * @param custom        The ConstraintAttribute used to set the value
      */
@@ -167,9 +170,12 @@
      * sets a oscillator wave point
      *
      * @param framePosition the position
+     * @param shape         the wave shape
+     * @param waveString    the wave string
      * @param variesBy      only varies by path supported for now
      * @param period        the period of the wave
      * @param offset        the offset value
+     * @param phase         the phase of the new wavepoint
      * @param value         the adder
      */
     public void setPoint(int framePosition,
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/CoreMotionScene.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/CoreMotionScene.java
index 5b7d71d..76b5128 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/CoreMotionScene.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/CoreMotionScene.java
@@ -25,6 +25,7 @@
      * set the Transitions string onto the MotionScene
      *
      * @param elementName the name of the element
+     * @param toJSON the json string of the transitioncontent
      */
     void setTransitionContent(String elementName, String toJSON);
 
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/Transition.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/Transition.java
index 98dbf1f..cf66b93 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/Transition.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/Transition.java
@@ -432,10 +432,11 @@
      * Converts from xy drag to progress
      * This should be used till touch up
      *
-     * @param baseW parent width
-     * @param baseH parent height
-     * @param dx    change in x
-     * @param dy    change in y
+     * @param currentProgress 0...1 progress in
+     * @param baseW           parent width
+     * @param baseH           parent height
+     * @param dx              change in x
+     * @param dy              change in y
      * @return the change in progress
      */
     public float dragToProgress(float currentProgress, int baseW, int baseH, float dx, float dy) {
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/Chain.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/Chain.java
index e06973f..a60a9d3 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/Chain.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/Chain.java
@@ -41,6 +41,8 @@
      *
      * @param constraintWidgetContainer root container
      * @param system                    the linear system we add the equations to
+     * @param widgets                   if this is null or contains any chainheads' widgets,
+     *                                  constrain all chains / the corresponding chains
      * @param orientation               HORIZONTAL or VERTICAL
      */
     public static void applyChainConstraints(
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidgetContainer.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidgetContainer.java
index 6644ed9d..af42a35 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidgetContainer.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidgetContainer.java
@@ -451,6 +451,7 @@
      * Update the frame of the layout and its children from the solver
      *
      * @param system the solver we get the values from.
+     * @param flags  the flag set associated with this solver
      */
     public boolean updateChildrenFromSolver(LinearSystem system, boolean[] flags) {
         flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = false;
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/DesignTool.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/DesignTool.java
index ef67572..8f96e8f 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/DesignTool.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/DesignTool.java
@@ -184,6 +184,7 @@
      *
      * @param view view to getMap the animation of
      * @param path array to be filled (x1,y1,x2,y2...)
+     * @param len  the desired number of point along animation
      * @return -1 if not under and animation 0 if not animated or number of point along animation
      */
     public int getAnimationPath(Object view, float[] path, int len) {
@@ -446,7 +447,8 @@
      * The call is designed to be efficient because it will be called 30x Number of views a second
      *
      * @param view the view to return keyframe positions
-     * @param info
+     * @param type if type is -1, skip all keyframes with type != -1
+     * @param info array to fill with info on each keyframe
      * @return Number of keyFrames found
      */
     public int getKeyFrameInfo(Object view, int type, int[] info) {
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
index 4dc61c2..a1cdd45 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
@@ -1675,6 +1675,7 @@
      * ...
      * length
      *
+     * @param type if type is -1, skip all keyframes with type != -1
      * @param info is a data structure array of int that holds info on each keyframe
      * @return Number of keyFrames found
      */
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
index f633da5a..b45ca83 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
@@ -3659,7 +3659,8 @@
      * Elevation logic is based on style and animation. By default it is not used because it would
      * lead to unexpected results.
      *
-     * @param apply true if this constraint set applies elevation to this view
+     * @param viewId ID of view to adjust the elevation
+     * @param apply  true if this constraint set applies elevation to this view
      */
     public void setApplyElevation(int viewId, boolean apply) {
         if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
@@ -4200,9 +4201,10 @@
     /**
      * Creates a ConstraintLayout Barrier object.
      *
-     * @param id
+     * @param id         the id of the constraint to create or partially overwrite.
      * @param direction  Barrier.{LEFT,RIGHT,TOP,BOTTOM,START,END}
-     * @param referenced
+     * @param margin     the barrierMargin of the Barrier object
+     * @param referenced the referenceIds of the Barrier object
      */
     public void createBarrier(int id, int direction, int margin, int... referenced) {
         Constraint constraint = get(id);
diff --git a/libraryversions.toml b/libraryversions.toml
index 3985367..59c4949 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -168,10 +168,10 @@
 WEAR_INPUT = "1.2.0-alpha03"
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha02"
-WEAR_PHONE_INTERACTIONS = "1.1.0-alpha05"
-WEAR_PROTOLAYOUT = "1.3.0-alpha06"
+WEAR_PHONE_INTERACTIONS = "1.1.0-beta01"
+WEAR_PROTOLAYOUT = "1.3.0-alpha07"
 WEAR_REMOTE_INTERACTIONS = "1.1.0-rc01"
-WEAR_TILES = "1.5.0-alpha06"
+WEAR_TILES = "1.5.0-alpha07"
 WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
 WEAR_WATCHFACE = "1.3.0-alpha05"
 WEBKIT = "1.13.0-alpha03"
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt b/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt
index b6bdb70..927612c 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt
@@ -1,10 +1,10 @@
 // Signature format: 4.0
 package androidx.lifecycle.viewmodel.navigation3 {
 
-  public final class ViewModelStoreNavContentWrapper implements androidx.navigation3.NavContentWrapper {
-    method @androidx.compose.runtime.Composable public void WrapBackStack(java.util.List<?> backStack);
-    method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
-    field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper INSTANCE;
+  public final class ViewModelStoreNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+    method @androidx.compose.runtime.Composable public void ProvideToBackStack(java.util.List<?> backStack);
+    method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+    field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavLocalProvider INSTANCE;
   }
 
 }
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt
index b6bdb70..927612c 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt
@@ -1,10 +1,10 @@
 // Signature format: 4.0
 package androidx.lifecycle.viewmodel.navigation3 {
 
-  public final class ViewModelStoreNavContentWrapper implements androidx.navigation3.NavContentWrapper {
-    method @androidx.compose.runtime.Composable public void WrapBackStack(java.util.List<?> backStack);
-    method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
-    field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper INSTANCE;
+  public final class ViewModelStoreNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+    method @androidx.compose.runtime.Composable public void ProvideToBackStack(java.util.List<?> backStack);
+    method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+    field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavLocalProvider INSTANCE;
   }
 
 }
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt b/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
index f0efd6c..f59f4fa 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
+++ b/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
@@ -26,9 +26,8 @@
 import androidx.lifecycle.createSavedStateHandle
 import androidx.lifecycle.viewmodel.compose.viewModel
 import androidx.navigation3.NavDisplay
-import androidx.navigation3.NavRecord
-import androidx.navigation3.SavedStateNavContentWrapper
-import androidx.navigation3.rememberNavWrapperManager
+import androidx.navigation3.NavEntry
+import androidx.navigation3.SavedStateNavLocalProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import kotlin.test.Test
@@ -37,65 +36,65 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-class ViewModelStoreNavContentWrapperTest {
+class ViewModelStoreNavLocalProviderTest {
     @get:Rule val composeTestRule = createComposeRule()
 
     @Test
     fun testViewModelProvided() {
-        val savedStateWrapper = SavedStateNavContentWrapper
-        val viewModelWrapper = ViewModelStoreNavContentWrapper
+        val savedStateWrapper = SavedStateNavLocalProvider
+        val viewModelWrapper = ViewModelStoreNavLocalProvider
         lateinit var viewModel1: MyViewModel
         lateinit var viewModel2: MyViewModel
-        val record1Arg = "record1 Arg"
-        val record2Arg = "record2 Arg"
-        val record1 =
-            NavRecord("key1") {
+        val entry1Arg = "entry1 Arg"
+        val entry2Arg = "entry2 Arg"
+        val entry1 =
+            NavEntry("key1") {
                 viewModel1 = viewModel<MyViewModel>()
-                viewModel1.myArg = record1Arg
+                viewModel1.myArg = entry1Arg
             }
-        val record2 =
-            NavRecord("key2") {
+        val entry2 =
+            NavEntry("key2") {
                 viewModel2 = viewModel<MyViewModel>()
-                viewModel2.myArg = record2Arg
+                viewModel2.myArg = entry2Arg
             }
         composeTestRule.setContent {
-            savedStateWrapper.WrapContent(
-                NavRecord(record1.key) { viewModelWrapper.WrapContent(record1) }
+            savedStateWrapper.ProvideToEntry(
+                NavEntry(entry1.key) { viewModelWrapper.ProvideToEntry(entry1) }
             )
-            savedStateWrapper.WrapContent(
-                NavRecord(record2.key) { viewModelWrapper.WrapContent(record2) }
+            savedStateWrapper.ProvideToEntry(
+                NavEntry(entry2.key) { viewModelWrapper.ProvideToEntry(entry2) }
             )
         }
 
         composeTestRule.runOnIdle {
-            assertWithMessage("Incorrect arg for record 1")
+            assertWithMessage("Incorrect arg for entry 1")
                 .that(viewModel1.myArg)
-                .isEqualTo(record1Arg)
-            assertWithMessage("Incorrect arg for record 2")
+                .isEqualTo(entry1Arg)
+            assertWithMessage("Incorrect arg for entry 2")
                 .that(viewModel2.myArg)
-                .isEqualTo(record2Arg)
+                .isEqualTo(entry2Arg)
         }
     }
 
     @Test
-    fun testViewModelNoSavedStateNavContentWrapper() {
-        val viewModelWrapper = ViewModelStoreNavContentWrapper
+    fun testViewModelNoSavedStateNavLocalProvider() {
+        val viewModelWrapper = ViewModelStoreNavLocalProvider
         lateinit var viewModel1: MyViewModel
-        val record1Arg = "record1 Arg"
-        val record1 =
-            NavRecord("key1") {
+        val entry1Arg = "entry1 Arg"
+        val entry1 =
+            NavEntry("key1") {
                 viewModel1 = viewModel<MyViewModel>()
-                viewModel1.myArg = record1Arg
+                viewModel1.myArg = entry1Arg
             }
         try {
-            composeTestRule.setContent { viewModelWrapper.WrapContent(record1) }
+            composeTestRule.setContent { viewModelWrapper.ProvideToEntry(entry1) }
         } catch (e: Exception) {
             assertThat(e)
                 .hasMessageThat()
                 .isEqualTo(
                     "The Lifecycle state is already beyond INITIALIZED. The " +
-                        "ViewModelStoreNavContentWrapper requires adding the " +
-                        "SavedStateNavContentWrapper to ensure support for " +
+                        "ViewModelStoreNavLocalProvider requires adding the " +
+                        "SavedStateNavLocalProvider to ensure support for " +
                         "SavedStateHandles."
                 )
         }
@@ -106,21 +105,17 @@
         lateinit var backStack: MutableList<Any>
         composeTestRule.setContent {
             backStack = remember { mutableStateListOf("Home") }
-            val manager =
-                rememberNavWrapperManager(
-                    listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
-                )
             NavDisplay(
                 backstack = backStack,
-                wrapperManager = manager,
+                localProviders = listOf(SavedStateNavLocalProvider, ViewModelStoreNavLocalProvider),
                 onBack = { backStack.removeAt(backStack.lastIndex) },
             ) { key ->
                 when (key) {
                     "Home" -> {
-                        NavRecord(key) { viewModel<HomeViewModel>() }
+                        NavEntry(key) { viewModel<HomeViewModel>() }
                     }
                     "AnotherScreen" -> {
-                        NavRecord(key) { viewModel<HomeViewModel>() }
+                        NavEntry(key) { viewModel<HomeViewModel>() }
                     }
                     else -> error("Unknown key: $key")
                 }
@@ -160,18 +155,14 @@
         lateinit var viewModel: SavedStateViewModel
         composeTestRule.setContent {
             backStack = remember { mutableStateListOf("Home") }
-            val manager =
-                rememberNavWrapperManager(
-                    listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
-                )
             NavDisplay(
                 backstack = backStack,
-                wrapperManager = manager,
+                localProviders = listOf(SavedStateNavLocalProvider, ViewModelStoreNavLocalProvider),
                 onBack = { backStack.removeAt(backStack.lastIndex) },
             ) { key ->
                 when (key) {
                     "Home" -> {
-                        NavRecord(key) {
+                        NavEntry(key) {
                             viewModel =
                                 viewModel<SavedStateViewModel> {
                                     val handle = createSavedStateHandle()
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt b/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
index 79402711..37b32ac 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
+++ b/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
@@ -35,44 +35,44 @@
 import androidx.lifecycle.viewmodel.MutableCreationExtras
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation3.NavContentWrapper
-import androidx.navigation3.NavRecord
+import androidx.navigation3.NavEntry
+import androidx.navigation3.NavLocalProvider
 import androidx.savedstate.SavedStateRegistryOwner
 import androidx.savedstate.compose.LocalSavedStateRegistryOwner
 
 /**
- * Provides the content of a [NavRecord] with a [ViewModelStoreOwner] and provides that
+ * Provides the content of a [NavEntry] with a [ViewModelStoreOwner] and provides that
  * [ViewModelStoreOwner] as a [LocalViewModelStoreOwner] so that it is available within the content.
  *
- * This requires that usage of the [SavedStateNavContentWrapper] to ensure that the [NavRecord]
- * scoped [ViewModel]s can properly provide access to [SavedStateHandle]s
+ * This requires that usage of the [SavedStateNavLocalProvider] to ensure that the [NavEntry] scoped
+ * [ViewModel]s can properly provide access to [SavedStateHandle]s
  */
-public object ViewModelStoreNavContentWrapper : NavContentWrapper {
+public object ViewModelStoreNavLocalProvider : NavLocalProvider {
 
     @Composable
-    override fun WrapBackStack(backStack: List<Any>) {
-        val recordViewModelStoreProvider = viewModel { RecordViewModel() }
-        recordViewModelStoreProvider.ownerInBackStack.clear()
-        recordViewModelStoreProvider.ownerInBackStack.addAll(backStack)
+    override fun ProvideToBackStack(backStack: List<Any>) {
+        val entryViewModelStoreProvider = viewModel { EntryViewModel() }
+        entryViewModelStoreProvider.ownerInBackStack.clear()
+        entryViewModelStoreProvider.ownerInBackStack.addAll(backStack)
     }
 
     @Composable
-    override fun <T : Any> WrapContent(record: NavRecord<T>) {
-        val key = record.key
-        val recordViewModelStoreProvider = viewModel { RecordViewModel() }
-        val viewModelStore = recordViewModelStoreProvider.viewModelStoreForKey(key)
+    override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
+        val key = entry.key
+        val entryViewModelStoreProvider = viewModel { EntryViewModel() }
+        val viewModelStore = entryViewModelStoreProvider.viewModelStoreForKey(key)
         // This ensures we always keep viewModels on config changes.
         val activity = LocalActivity.current
         remember(key, viewModelStore) {
             object : RememberObserver {
                 override fun onAbandoned() {
-                    if (!recordViewModelStoreProvider.ownerInBackStack.contains(key)) {
+                    if (!entryViewModelStoreProvider.ownerInBackStack.contains(key)) {
                         disposeIfNotChangingConfiguration()
                     }
                 }
 
                 override fun onForgotten() {
-                    if (!recordViewModelStoreProvider.ownerInBackStack.contains(key)) {
+                    if (!entryViewModelStoreProvider.ownerInBackStack.contains(key)) {
                         disposeIfNotChangingConfiguration()
                     }
                 }
@@ -81,7 +81,7 @@
 
                 fun disposeIfNotChangingConfiguration() {
                     if (activity?.isChangingConfigurations != true) {
-                        recordViewModelStoreProvider.removeViewModelStoreOwnerForKey(key)?.clear()
+                        entryViewModelStoreProvider.removeViewModelStoreOwnerForKey(key)?.clear()
                     }
                 }
             }
@@ -110,20 +110,20 @@
                     init {
                         require(this.lifecycle.currentState == Lifecycle.State.INITIALIZED) {
                             "The Lifecycle state is already beyond INITIALIZED. The " +
-                                "ViewModelStoreNavContentWrapper requires adding the " +
-                                "SavedStateNavContentWrapper to ensure support for " +
+                                "ViewModelStoreNavLocalProvider requires adding the " +
+                                "SavedStateNavLocalProvider to ensure support for " +
                                 "SavedStateHandles."
                         }
                         enableSavedStateHandles()
                     }
                 }
         ) {
-            record.content.invoke(key)
+            entry.content.invoke(key)
         }
     }
 }
 
-private class RecordViewModel : ViewModel() {
+private class EntryViewModel : ViewModel() {
     private val owners = mutableMapOf<Any, ViewModelStore>()
     val ownerInBackStack = mutableListOf<Any>()
 
diff --git a/navigation3/navigation3/api/current.txt b/navigation3/navigation3/api/current.txt
index ec7e433..d8d07e0 100644
--- a/navigation3/navigation3/api/current.txt
+++ b/navigation3/navigation3/api/current.txt
@@ -1,9 +1,48 @@
 // Signature format: 4.0
 package androidx.navigation3 {
 
-  public interface NavContentWrapper {
-    method @androidx.compose.runtime.Composable public default void WrapBackStack(java.util.List<?> backStack);
-    method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
+  public final class EntryClassProvider<T> {
+    ctor public EntryClassProvider(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public kotlin.reflect.KClass<T> component1();
+    method public java.util.Map<java.lang.String,java.lang.Object> component2();
+    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
+    method public androidx.navigation3.EntryClassProvider<T> copy(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public kotlin.reflect.KClass<T> getClazz();
+    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
+    method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+    property public final kotlin.reflect.KClass<T> clazz;
+    property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
+    property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+  }
+
+  @kotlin.DslMarker public @interface EntryDsl {
+  }
+
+  public final class EntryProvider<T> {
+    ctor public EntryProvider(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public T component1();
+    method public java.util.Map<java.lang.String,java.lang.Object> component2();
+    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
+    method public androidx.navigation3.EntryProvider<T> copy(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
+    method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+    method public T getKey();
+    property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
+    property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+    property public final T key;
+  }
+
+  @androidx.navigation3.EntryDsl public final class EntryProviderBuilder {
+    ctor public EntryProviderBuilder(kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavEntry<? extends java.lang.Object?>> fallback);
+    method public <T> void addEntryProvider(kotlin.reflect.KClass<T> clazz, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public <T> void addEntryProvider(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavEntry<? extends java.lang.Object?>> build();
+  }
+
+  public final class EntryProviderKt {
+    method public static inline <reified T> void entry(androidx.navigation3.EntryProviderBuilder, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public static <T> void entry(androidx.navigation3.EntryProviderBuilder, T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public static inline kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavEntry<? extends java.lang.Object?>> entryProvider(optional kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavEntry<? extends java.lang.Object?>> fallback, kotlin.jvm.functions.Function1<? super androidx.navigation3.EntryProviderBuilder,kotlin.Unit> builder);
   }
 
   public final class NavDisplay {
@@ -13,11 +52,11 @@
   }
 
   public final class NavDisplay_androidKt {
-    method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
+    method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional java.util.List<? extends androidx.navigation3.NavLocalProvider> localProviders, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavEntry<? extends T>> entryProvider);
   }
 
-  public final class NavRecord<T> {
-    ctor public NavRecord(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+  public final class NavEntry<T> {
+    ctor public NavEntry(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
     method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
     method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
     method public T getKey();
@@ -26,69 +65,30 @@
     property public final T key;
   }
 
+  public interface NavLocalProvider {
+    method @androidx.compose.runtime.Composable public default void ProvideToBackStack(java.util.List<?> backStack);
+    method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+  }
+
   public final class NavWrapperManager {
     ctor public NavWrapperManager();
-    ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
-    method @androidx.compose.runtime.Composable public <T> void ContentForRecord(androidx.navigation3.NavRecord<T> record);
+    ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavLocalProvider> navLocalProviders);
+    method @androidx.compose.runtime.Composable public <T> void ContentForEntry(androidx.navigation3.NavEntry<T> entry);
     method @androidx.compose.runtime.Composable public void PrepareBackStack(java.util.List<?> backStack);
   }
 
   public final class NavWrapperManagerKt {
-    method @androidx.compose.runtime.Composable public static androidx.navigation3.NavWrapperManager rememberNavWrapperManager(java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
+    method @androidx.compose.runtime.Composable public static androidx.navigation3.NavWrapperManager rememberNavWrapperManager(java.util.List<? extends androidx.navigation3.NavLocalProvider> navLocalProviders);
   }
 
-  public final class RecordClassProvider<T> {
-    ctor public RecordClassProvider(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public kotlin.reflect.KClass<T> component1();
-    method public java.util.Map<java.lang.String,java.lang.Object> component2();
-    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
-    method public androidx.navigation3.RecordClassProvider<T> copy(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public kotlin.reflect.KClass<T> getClazz();
-    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
-    method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
-    property public final kotlin.reflect.KClass<T> clazz;
-    property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
-    property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+  public final class SaveableStateNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+    ctor public SaveableStateNavLocalProvider();
+    method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
   }
 
-  @kotlin.DslMarker public @interface RecordDsl {
-  }
-
-  public final class RecordProvider<T> {
-    ctor public RecordProvider(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public T component1();
-    method public java.util.Map<java.lang.String,java.lang.Object> component2();
-    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
-    method public androidx.navigation3.RecordProvider<T> copy(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
-    method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
-    method public T getKey();
-    property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
-    property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
-    property public final T key;
-  }
-
-  @androidx.navigation3.RecordDsl public final class RecordProviderBuilder {
-    ctor public RecordProviderBuilder(kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavRecord<? extends java.lang.Object?>> fallback);
-    method public <T> void addRecordProvider(kotlin.reflect.KClass<T> clazz, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public <T> void addRecordProvider(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavRecord<? extends java.lang.Object?>> build();
-  }
-
-  public final class RecordProviderKt {
-    method public static inline <reified T> void record(androidx.navigation3.RecordProviderBuilder, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public static <T> void record(androidx.navigation3.RecordProviderBuilder, T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public static inline kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavRecord<? extends java.lang.Object?>> recordProvider(optional kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavRecord<? extends java.lang.Object?>> fallback, kotlin.jvm.functions.Function1<? super androidx.navigation3.RecordProviderBuilder,kotlin.Unit> builder);
-  }
-
-  public final class SaveableStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
-    ctor public SaveableStateNavContentWrapper();
-    method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
-  }
-
-  public final class SavedStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
-    method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
-    field public static final androidx.navigation3.SavedStateNavContentWrapper INSTANCE;
+  public final class SavedStateNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+    method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+    field public static final androidx.navigation3.SavedStateNavLocalProvider INSTANCE;
   }
 
 }
diff --git a/navigation3/navigation3/api/restricted_current.txt b/navigation3/navigation3/api/restricted_current.txt
index ec7e433..d8d07e0 100644
--- a/navigation3/navigation3/api/restricted_current.txt
+++ b/navigation3/navigation3/api/restricted_current.txt
@@ -1,9 +1,48 @@
 // Signature format: 4.0
 package androidx.navigation3 {
 
-  public interface NavContentWrapper {
-    method @androidx.compose.runtime.Composable public default void WrapBackStack(java.util.List<?> backStack);
-    method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
+  public final class EntryClassProvider<T> {
+    ctor public EntryClassProvider(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public kotlin.reflect.KClass<T> component1();
+    method public java.util.Map<java.lang.String,java.lang.Object> component2();
+    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
+    method public androidx.navigation3.EntryClassProvider<T> copy(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public kotlin.reflect.KClass<T> getClazz();
+    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
+    method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+    property public final kotlin.reflect.KClass<T> clazz;
+    property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
+    property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+  }
+
+  @kotlin.DslMarker public @interface EntryDsl {
+  }
+
+  public final class EntryProvider<T> {
+    ctor public EntryProvider(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public T component1();
+    method public java.util.Map<java.lang.String,java.lang.Object> component2();
+    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
+    method public androidx.navigation3.EntryProvider<T> copy(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
+    method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+    method public T getKey();
+    property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
+    property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+    property public final T key;
+  }
+
+  @androidx.navigation3.EntryDsl public final class EntryProviderBuilder {
+    ctor public EntryProviderBuilder(kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavEntry<? extends java.lang.Object?>> fallback);
+    method public <T> void addEntryProvider(kotlin.reflect.KClass<T> clazz, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public <T> void addEntryProvider(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavEntry<? extends java.lang.Object?>> build();
+  }
+
+  public final class EntryProviderKt {
+    method public static inline <reified T> void entry(androidx.navigation3.EntryProviderBuilder, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public static <T> void entry(androidx.navigation3.EntryProviderBuilder, T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+    method public static inline kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavEntry<? extends java.lang.Object?>> entryProvider(optional kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavEntry<? extends java.lang.Object?>> fallback, kotlin.jvm.functions.Function1<? super androidx.navigation3.EntryProviderBuilder,kotlin.Unit> builder);
   }
 
   public final class NavDisplay {
@@ -13,11 +52,11 @@
   }
 
   public final class NavDisplay_androidKt {
-    method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
+    method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional java.util.List<? extends androidx.navigation3.NavLocalProvider> localProviders, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavEntry<? extends T>> entryProvider);
   }
 
-  public final class NavRecord<T> {
-    ctor public NavRecord(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+  public final class NavEntry<T> {
+    ctor public NavEntry(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
     method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
     method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
     method public T getKey();
@@ -26,69 +65,30 @@
     property public final T key;
   }
 
+  public interface NavLocalProvider {
+    method @androidx.compose.runtime.Composable public default void ProvideToBackStack(java.util.List<?> backStack);
+    method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+  }
+
   public final class NavWrapperManager {
     ctor public NavWrapperManager();
-    ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
-    method @androidx.compose.runtime.Composable public <T> void ContentForRecord(androidx.navigation3.NavRecord<T> record);
+    ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavLocalProvider> navLocalProviders);
+    method @androidx.compose.runtime.Composable public <T> void ContentForEntry(androidx.navigation3.NavEntry<T> entry);
     method @androidx.compose.runtime.Composable public void PrepareBackStack(java.util.List<?> backStack);
   }
 
   public final class NavWrapperManagerKt {
-    method @androidx.compose.runtime.Composable public static androidx.navigation3.NavWrapperManager rememberNavWrapperManager(java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
+    method @androidx.compose.runtime.Composable public static androidx.navigation3.NavWrapperManager rememberNavWrapperManager(java.util.List<? extends androidx.navigation3.NavLocalProvider> navLocalProviders);
   }
 
-  public final class RecordClassProvider<T> {
-    ctor public RecordClassProvider(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public kotlin.reflect.KClass<T> component1();
-    method public java.util.Map<java.lang.String,java.lang.Object> component2();
-    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
-    method public androidx.navigation3.RecordClassProvider<T> copy(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public kotlin.reflect.KClass<T> getClazz();
-    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
-    method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
-    property public final kotlin.reflect.KClass<T> clazz;
-    property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
-    property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+  public final class SaveableStateNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+    ctor public SaveableStateNavLocalProvider();
+    method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
   }
 
-  @kotlin.DslMarker public @interface RecordDsl {
-  }
-
-  public final class RecordProvider<T> {
-    ctor public RecordProvider(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public T component1();
-    method public java.util.Map<java.lang.String,java.lang.Object> component2();
-    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
-    method public androidx.navigation3.RecordProvider<T> copy(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
-    method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
-    method public T getKey();
-    property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
-    property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
-    property public final T key;
-  }
-
-  @androidx.navigation3.RecordDsl public final class RecordProviderBuilder {
-    ctor public RecordProviderBuilder(kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavRecord<? extends java.lang.Object?>> fallback);
-    method public <T> void addRecordProvider(kotlin.reflect.KClass<T> clazz, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public <T> void addRecordProvider(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavRecord<? extends java.lang.Object?>> build();
-  }
-
-  public final class RecordProviderKt {
-    method public static inline <reified T> void record(androidx.navigation3.RecordProviderBuilder, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public static <T> void record(androidx.navigation3.RecordProviderBuilder, T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
-    method public static inline kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavRecord<? extends java.lang.Object?>> recordProvider(optional kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavRecord<? extends java.lang.Object?>> fallback, kotlin.jvm.functions.Function1<? super androidx.navigation3.RecordProviderBuilder,kotlin.Unit> builder);
-  }
-
-  public final class SaveableStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
-    ctor public SaveableStateNavContentWrapper();
-    method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
-  }
-
-  public final class SavedStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
-    method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
-    field public static final androidx.navigation3.SavedStateNavContentWrapper INSTANCE;
+  public final class SavedStateNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+    method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+    field public static final androidx.navigation3.SavedStateNavLocalProvider INSTANCE;
   }
 
 }
diff --git a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
index fa6ff47..d4728e8 100644
--- a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
+++ b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
@@ -23,13 +23,12 @@
 import androidx.compose.runtime.Composable
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper
+import androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavLocalProvider
 import androidx.navigation3.NavDisplay
-import androidx.navigation3.NavRecord
-import androidx.navigation3.SavedStateNavContentWrapper
-import androidx.navigation3.record
-import androidx.navigation3.recordProvider
-import androidx.navigation3.rememberNavWrapperManager
+import androidx.navigation3.NavEntry
+import androidx.navigation3.SavedStateNavLocalProvider
+import androidx.navigation3.entry
+import androidx.navigation3.entryProvider
 
 class ProfileViewModel : ViewModel() {
     val name = "no user"
@@ -39,31 +38,27 @@
 @Composable
 fun BaseNav() {
     val backStack = rememberMutableStateListOf(Profile)
-    val manager =
-        rememberNavWrapperManager(
-            listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
-        )
     NavDisplay(
         backstack = backStack,
-        wrapperManager = manager,
+        localProviders = listOf(SavedStateNavLocalProvider, ViewModelStoreNavLocalProvider),
         onBack = { backStack.removeLast() },
-        recordProvider =
-            recordProvider({ NavRecord(Unit) { Text(text = "Invalid Key") } }) {
-                record<Profile>(
+        entryProvider =
+            entryProvider({ NavEntry(Unit) { Text(text = "Invalid Key") } }) {
+                entry<Profile>(
                     NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
                 ) {
                     val viewModel = viewModel<ProfileViewModel>()
                     Profile(viewModel, { backStack.add(it) }) { backStack.removeLast() }
                 }
-                record<Scrollable>(
+                entry<Scrollable>(
                     NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
                 ) {
                     Scrollable({ backStack.add(it) }) { backStack.removeLast() }
                 }
-                record<Dialog>(featureMap = NavDisplay.isDialog(true)) {
+                entry<Dialog>(featureMap = NavDisplay.isDialog(true)) {
                     DialogContent { backStack.removeLast() }
                 }
-                record<Dashboard>(
+                entry<Dashboard>(
                     NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
                 ) { dashboardArgs ->
                     val userId = dashboardArgs.userId
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
index d7da926..6fb5955 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
@@ -49,8 +49,8 @@
             backstack = remember { mutableStateListOf(first) }
             NavDisplay(backstack) {
                 when (it) {
-                    first -> NavRecord(first) { Text(first) }
-                    second -> NavRecord(second) { Text(second) }
+                    first -> NavEntry(first) { Text(first) }
+                    second -> NavEntry(second) { Text(second) }
                     else -> error("Invalid key passed")
                 }
             }
@@ -93,13 +93,13 @@
             NavDisplay(backstack) {
                 when (it) {
                     first ->
-                        NavRecord(
+                        NavEntry(
                             first,
                         ) {
                             Text(first)
                         }
                     second ->
-                        NavRecord(
+                        NavEntry(
                             second,
                             featureMap =
                                 NavDisplay.transition(
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
index fd5749a..614eb88 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
@@ -34,6 +34,7 @@
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertWithMessage
 import kotlin.test.Test
+import kotlin.test.assertFailsWith
 import org.junit.Rule
 import org.junit.runner.RunWith
 
@@ -45,7 +46,7 @@
     @Test
     fun testContentShown() {
         composeTestRule.setContent {
-            NavDisplay(backstack = mutableStateListOf(first)) { NavRecord(first) { Text(first) } }
+            NavDisplay(backstack = mutableStateListOf(first)) { NavEntry(first) { Text(first) } }
         }
 
         assertThat(composeTestRule.onNodeWithText(first).isDisplayed()).isTrue()
@@ -58,8 +59,8 @@
             backstack = remember { mutableStateListOf(first) }
             NavDisplay(backstack = backstack) {
                 when (it) {
-                    first -> NavRecord(first) { Text(first) }
-                    second -> NavRecord(second) { Text(second) }
+                    first -> NavEntry(first) { Text(first) }
+                    second -> NavEntry(second) { Text(second) }
                     else -> error("Invalid key passed")
                 }
             }
@@ -80,8 +81,8 @@
             backstack = remember { mutableStateListOf(first) }
             NavDisplay(backstack = backstack) {
                 when (it) {
-                    first -> NavRecord(first) { Text(first) }
-                    second -> NavRecord(second, NavDisplay.isDialog(true)) { Text(second) }
+                    first -> NavEntry(first) { Text(first) }
+                    second -> NavEntry(second, NavDisplay.isDialog(true)) { Text(second) }
                     else -> error("Invalid key passed")
                 }
             }
@@ -105,8 +106,8 @@
             backstack = remember { mutableStateListOf(first) }
             NavDisplay(backstack = backstack) {
                 when (it) {
-                    first -> NavRecord(first) { Text(first) }
-                    second -> NavRecord(second) { Text(second) }
+                    first -> NavEntry(first) { Text(first) }
+                    second -> NavEntry(second) { Text(second) }
                     else -> error("Invalid key passed")
                 }
             }
@@ -132,8 +133,8 @@
             backstack = remember { mutableStateListOf(first) }
             NavDisplay(backstack = backstack) {
                 when (it) {
-                    first -> NavRecord(first) { numberOnScreen1 = rememberSaveable { increment++ } }
-                    second -> NavRecord(second) {}
+                    first -> NavEntry(first) { numberOnScreen1 = rememberSaveable { increment++ } }
+                    second -> NavEntry(second) {}
                     else -> error("Invalid key passed")
                 }
             }
@@ -142,14 +143,11 @@
         composeTestRule.runOnIdle {
             assertWithMessage("Initial number should be 0").that(numberOnScreen1).isEqualTo(0)
             numberOnScreen1 = -1
+            assertWithMessage("The number should be -1").that(numberOnScreen1).isEqualTo(-1)
             backstack.add(second)
         }
 
-        composeTestRule.runOnIdle {
-            assertWithMessage("The number should be -1").that(numberOnScreen1).isEqualTo(-1)
-            // removeLast requires API 35
-            backstack.removeAt(backstack.size - 1)
-        }
+        composeTestRule.runOnIdle { backstack.removeAt(backstack.size - 1) }
 
         composeTestRule.runOnIdle {
             assertWithMessage("The number should be restored").that(numberOnScreen1).isEqualTo(0)
@@ -165,15 +163,14 @@
         composeTestRule.setContent {
             mainRegistry = LocalSavedStateRegistryOwner.current.savedStateRegistry
             backstack = remember { mutableStateListOf(first) }
-            val manager = rememberNavWrapperManager(listOf(SavedStateNavContentWrapper))
-            NavDisplay(backstack = backstack, wrapperManager = manager) {
+            NavDisplay(backstack = backstack, localProviders = listOf(SavedStateNavLocalProvider)) {
                 when (it) {
                     first ->
-                        NavRecord(first) {
+                        NavEntry(first) {
                             registry1 = LocalSavedStateRegistryOwner.current.savedStateRegistry
                         }
                     second ->
-                        NavRecord(second) {
+                        NavEntry(second) {
                             registry2 = LocalSavedStateRegistryOwner.current.savedStateRegistry
                         }
                     else -> error("Invalid key passed")
@@ -212,12 +209,12 @@
                         2 -> backStack2
                         else -> backStack3
                     },
-                recordProvider =
-                    recordProvider {
-                        record(first) { Text(first) }
-                        record(second) { Text(second) }
-                        record(third) { Text(third) }
-                        record(forth) { Text(forth) }
+                entryProvider =
+                    entryProvider {
+                        entry(first) { Text(first) }
+                        entry(second) { Text(second) }
+                        entry(third) { Text(third) }
+                        entry(forth) { Text(forth) }
                     }
             )
         }
@@ -238,6 +235,43 @@
         assertThat(backStack3).containsExactly(third, forth)
         assertThat(composeTestRule.onNodeWithText(forth).isDisplayed()).isTrue()
     }
+
+    @Test
+    fun testInitEmptyBackstackThrows() {
+        lateinit var backstack: MutableList<Any>
+        val fail =
+            assertFailsWith<IllegalArgumentException> {
+                composeTestRule.setContent {
+                    backstack = remember { mutableStateListOf() }
+                    NavDisplay(backstack = backstack) { NavEntry(first) {} }
+                }
+            }
+        assertThat(fail.message).isEqualTo("NavDisplay backstack cannot be empty")
+    }
+
+    @Test
+    fun testPopToEmptyBackstackThrows() {
+        lateinit var backstack: MutableList<Any>
+        composeTestRule.setContent {
+            backstack = remember { mutableStateListOf(first) }
+            NavDisplay(backstack = backstack) {
+                when (it) {
+                    first -> NavEntry(first) { Text(first) }
+                    second -> NavEntry(second) { Text(second) }
+                    else -> error("Invalid key passed")
+                }
+            }
+        }
+
+        assertThat(composeTestRule.onNodeWithText(first).isDisplayed()).isTrue()
+
+        val fail =
+            assertFailsWith<IllegalArgumentException> {
+                composeTestRule.runOnIdle { backstack.clear() }
+                composeTestRule.waitForIdle()
+            }
+        assertThat(fail.message).isEqualTo("NavDisplay backstack cannot be empty")
+    }
 }
 
 private const val first = "first"
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavWrapperManagerTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavWrapperManagerTest.kt
index be41543..646df38 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavWrapperManagerTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavWrapperManagerTest.kt
@@ -35,14 +35,14 @@
         var calledWrapBackStack = false
         var calledWrapContent = false
         val wrapper =
-            object : NavContentWrapper {
+            object : NavLocalProvider {
                 @Composable
-                override fun WrapBackStack(backStack: List<Any>) {
+                override fun ProvideToBackStack(backStack: List<Any>) {
                     calledWrapBackStack = true
                 }
 
                 @Composable
-                override fun <T : Any> WrapContent(record: NavRecord<T>) {
+                override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
                     calledWrapContent = true
                 }
             }
@@ -51,7 +51,7 @@
 
         composeTestRule.setContent {
             manager.PrepareBackStack(listOf("something"))
-            manager.ContentForRecord(NavRecord("myKey") {})
+            manager.ContentForEntry(NavEntry("myKey") {})
         }
 
         assertThat(calledWrapBackStack).isTrue()
@@ -63,14 +63,14 @@
         var calledWrapBackStackCount = 0
         var calledWrapContentCount = 0
         val wrapper =
-            object : NavContentWrapper {
+            object : NavLocalProvider {
                 @Composable
-                override fun WrapBackStack(backStack: List<Any>) {
+                override fun ProvideToBackStack(backStack: List<Any>) {
                     calledWrapBackStackCount++
                 }
 
                 @Composable
-                override fun <T : Any> WrapContent(record: NavRecord<T>) {
+                override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
                     calledWrapContentCount++
                 }
             }
@@ -79,7 +79,7 @@
 
         composeTestRule.setContent {
             manager.PrepareBackStack(listOf("something"))
-            manager.ContentForRecord(NavRecord("myKey") {})
+            manager.ContentForEntry(NavEntry("myKey") {})
         }
 
         assertThat(calledWrapBackStackCount).isEqualTo(1)
diff --git a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
index cdcff77..fdc1e24 100644
--- a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
+++ b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
@@ -34,7 +34,7 @@
 /** Object that indicates the features that can be handled by the [NavDisplay] */
 public object NavDisplay {
     /**
-     * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
+     * Function to be called on the [NavEntry.featureMap] to notify the [NavDisplay] that the
      * content should be animated using the provided transitions.
      */
     public fun transition(enter: EnterTransition?, exit: ExitTransition?): Map<String, Any> =
@@ -42,7 +42,7 @@
         else mapOf(ENTER_TRANSITION_KEY to enter, EXIT_TRANSITION_KEY to exit)
 
     /**
-     * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
+     * Function to be called on the [NavEntry.featureMap] to notify the [NavDisplay] that the
      * content should be displayed inside of a [Dialog]
      */
     public fun isDialog(boolean: Boolean): Map<String, Any> =
@@ -60,29 +60,28 @@
  *
  * The NavDisplay displays the content associated with the last key on the back stack in most
  * circumstances. If that content wants to be displayed as a dialog, as communicated by adding
- * [NavDisplay.isDialog] to a [NavRecord.featureMap], then the last key's content is a dialog and
- * the second to last key is a displayed in the background.
+ * [NavDisplay.isDialog] to a [NavEntry.featureMap], then the last key's content is a dialog and the
+ * second to last key is a displayed in the background.
  *
  * @param backstack the collection of keys that represents the state that needs to be handled
- * @param wrapperManager the manager that combines all of the [NavContentWrapper]s
+ * @param localProviders list of [NavLocalProvider] to add information to the provided entriess
  * @param modifier the modifier to be applied to the layout.
  * @param contentAlignment The [Alignment] of the [AnimatedContent]
- *     * @param enterTransition Default [EnterTransition] for all [NavRecord]s. Can be overridden
- *     * individually for each [NavRecord] by passing in the record's transitions through
- *     * [NavRecord.featureMap].
- *     * @param exitTransition Default [ExitTransition] for all [NavRecord]s. Can be overridden
- *     * individually for each [NavRecord] by passing in the record's transitions through
- *     * [NavRecord.featureMap].
- *
+ * @param enterTransition Default [EnterTransition] for all [NavEntry]s. Can be overridden
+ *   individually for each [NavEntry] by passing in the entry's transitions through
+ *   [NavEntry.featureMap].
+ * @param exitTransition Default [ExitTransition] for all [NavEntry]s. Can be overridden
+ *   individually for each [NavEntry] by passing in the entry's transitions through
+ *   [NavEntry.featureMap].
  * @param onBack a callback for handling system back presses
- * @param recordProvider lambda used to construct each possible [NavRecord]
+ * @param entryProvider lambda used to construct each possible [NavEntry]
  * @sample androidx.navigation3.samples.BaseNav
  */
 @Composable
 public fun <T : Any> NavDisplay(
     backstack: List<T>,
     modifier: Modifier = Modifier,
-    wrapperManager: NavWrapperManager = rememberNavWrapperManager(emptyList()),
+    localProviders: List<NavLocalProvider> = emptyList(),
     contentAlignment: Alignment = Alignment.TopStart,
     sizeTransform: SizeTransform? = null,
     enterTransition: EnterTransition =
@@ -100,33 +99,36 @@
                 )
         ),
     onBack: () -> Unit = { if (backstack is MutableList) backstack.removeAt(backstack.size - 1) },
-    recordProvider: (key: T) -> NavRecord<out T>
+    entryProvider: (key: T) -> NavEntry<out T>
 ) {
+    require(backstack.isNotEmpty()) { "NavDisplay backstack cannot be empty" }
+
+    val wrapperManager: NavWrapperManager = rememberNavWrapperManager(localProviders)
     BackHandler(backstack.size > 1, onBack)
     wrapperManager.PrepareBackStack(backStack = backstack)
     val key = backstack.last()
-    val record = recordProvider.invoke(key)
+    val entry = entryProvider.invoke(key)
 
-    // Incoming record defines transitions, otherwise it uses default transitions from NavDisplay
+    // Incoming entry defines transitions, otherwise it uses default transitions from NavDisplay
     val finalEnterTransition =
-        record.featureMap[NavDisplay.ENTER_TRANSITION_KEY] as? EnterTransition ?: enterTransition
+        entry.featureMap[NavDisplay.ENTER_TRANSITION_KEY] as? EnterTransition ?: enterTransition
     val finalExitTransition =
-        record.featureMap[NavDisplay.EXIT_TRANSITION_KEY] as? ExitTransition ?: exitTransition
+        entry.featureMap[NavDisplay.EXIT_TRANSITION_KEY] as? ExitTransition ?: exitTransition
 
-    val isDialog = record.featureMap[NavDisplay.DIALOG_KEY] == true
+    val isDialog = entry.featureMap[NavDisplay.DIALOG_KEY] == true
 
     // if there is a dialog, we should create a transition with the next to last entry instead.
     val transition =
         if (isDialog) {
             if (backstack.size > 1) {
                 val previousKey = backstack[backstack.size - 2]
-                val previousRecord = recordProvider.invoke(previousKey)
-                updateTransition(targetState = previousRecord, label = previousKey.toString())
+                val previousEntry = entryProvider.invoke(previousKey)
+                updateTransition(targetState = previousEntry, label = previousKey.toString())
             } else {
                 null
             }
         } else {
-            updateTransition(targetState = record, label = key.toString())
+            updateTransition(targetState = entry, label = key.toString())
         }
 
     transition?.AnimatedContent(
@@ -140,11 +142,11 @@
         },
         contentAlignment = contentAlignment,
         contentKey = { it.key }
-    ) { innerRecord ->
-        wrapperManager.ContentForRecord(innerRecord)
+    ) { innerEntry ->
+        wrapperManager.ContentForEntry(innerEntry)
     }
 
     if (isDialog) {
-        Dialog(onBack) { wrapperManager.ContentForRecord(record) }
+        Dialog(onBack) { wrapperManager.ContentForEntry(entry) }
     }
 }
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/EntryProvider.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/EntryProvider.kt
new file mode 100644
index 0000000..47471e4
--- /dev/null
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/EntryProvider.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.navigation3
+
+import androidx.compose.runtime.Composable
+import kotlin.reflect.KClass
+
+@DslMarker public annotation class EntryDsl
+
+/** Creates an [EntryProviderBuilder] with the entry providers provided in the builder. */
+public inline fun entryProvider(
+    noinline fallback: (unknownScreen: Any) -> NavEntry<*> = {
+        throw IllegalStateException("Unknown screen $it")
+    },
+    builder: EntryProviderBuilder.() -> Unit
+): (Any) -> NavEntry<*> = EntryProviderBuilder(fallback).apply(builder).build()
+
+/** DSL for constructing a new [NavEntry] */
+@Suppress("TopLevelBuilder")
+@EntryDsl
+public class EntryProviderBuilder(private val fallback: (unknownScreen: Any) -> NavEntry<*>) {
+    private val clazzProviders = mutableMapOf<KClass<*>, EntryClassProvider<*>>()
+    private val providers = mutableMapOf<Any, EntryProvider<*>>()
+
+    /** Builds a [NavEntry] for the given [key] that displays [content]. */
+    @Suppress("SetterReturnsThis", "MissingGetterMatchingBuilder")
+    public fun <T : Any> addEntryProvider(
+        key: T,
+        featureMap: Map<String, Any> = emptyMap(),
+        content: @Composable (T) -> Unit,
+    ) {
+        require(key !in providers) {
+            "An `entry` with the key `key` has already been added: ${key}."
+        }
+        providers[key] = EntryProvider(key, featureMap, content)
+    }
+
+    /** Builds a [NavEntry] for the given [clazz] that displays [content]. */
+    @Suppress("SetterReturnsThis", "MissingGetterMatchingBuilder")
+    public fun <T : Any> addEntryProvider(
+        clazz: KClass<T>,
+        featureMap: Map<String, Any> = emptyMap(),
+        content: @Composable (T) -> Unit,
+    ) {
+        require(clazz !in clazzProviders) {
+            "An `entry` with the same `clazz` has already been added: ${clazz.simpleName}."
+        }
+        clazzProviders[clazz] = EntryClassProvider(clazz, featureMap, content)
+    }
+
+    /**
+     * Returns an instance of entryProvider created from the entry providers set on this builder.
+     */
+    @Suppress("UNCHECKED_CAST")
+    public fun build(): (Any) -> NavEntry<*> = { key ->
+        val entryClassProvider = clazzProviders[key::class] as? EntryClassProvider<Any>
+        val entryProvider = providers[key] as? EntryProvider<Any>
+        entryClassProvider?.run { NavEntry(key, featureMap, content) }
+            ?: entryProvider?.run { NavEntry(key, featureMap, content) }
+            ?: fallback.invoke(key)
+    }
+}
+
+/** Add an entry provider to the [EntryProviderBuilder] */
+public fun <T : Any> EntryProviderBuilder.entry(
+    key: T,
+    featureMap: Map<String, Any> = emptyMap(),
+    content: @Composable (T) -> Unit,
+) {
+    addEntryProvider(key, featureMap, content)
+}
+
+/** Add an entry provider to the [EntryProviderBuilder] */
+public inline fun <reified T : Any> EntryProviderBuilder.entry(
+    featureMap: Map<String, Any> = emptyMap(),
+    noinline content: @Composable (T) -> Unit,
+) {
+    addEntryProvider(T::class, featureMap, content)
+}
+
+/** Holds a Entry class, featureMap, and content for that class */
+public data class EntryClassProvider<T : Any>(
+    val clazz: KClass<T>,
+    val featureMap: Map<String, Any>,
+    val content: @Composable (T) -> Unit,
+)
+
+/** Holds a Entry class, featureMap, and content for that key */
+public data class EntryProvider<T : Any>(
+    val key: T,
+    val featureMap: Map<String, Any>,
+    val content: @Composable (T) -> Unit,
+)
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavRecord.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavEntry.kt
similarity index 72%
rename from navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavRecord.kt
rename to navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavEntry.kt
index 71cd4e0..e5d19d9 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavRecord.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavEntry.kt
@@ -19,14 +19,14 @@
 import androidx.compose.runtime.Composable
 
 /**
- * Record maintains the store the key and the content represented by that key. Records should be
- * created as part of a [NavDisplay.recordProvider](reference/androidx/navigation/NavDisplay).
+ * Entry maintains and stores the key and the content represented by that key. Entries should be
+ * created as part of a [NavDisplay.entryProvider](reference/androidx/navigation/NavDisplay).
  *
- * @param key key for this record
+ * @param key key for this entry
  * @param featureMap map of the available features from a display
- * @param content content for this record to be displayed when this record is active
+ * @param content content for this entry to be displayed when this entry is active
  */
-public class NavRecord<T : Any>(
+public class NavEntry<T : Any>(
     public val key: T,
     public val featureMap: Map<String, Any> = emptyMap(),
     public val content: @Composable (T) -> Unit,
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavContentWrapper.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavLocalProvider.kt
similarity index 73%
rename from navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavContentWrapper.kt
rename to navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavLocalProvider.kt
index 42d1652..b3fbeb2 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavContentWrapper.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavLocalProvider.kt
@@ -22,23 +22,23 @@
  * Interface that offers the ability to provide information to some Composable content that is
  * integrated with a [NavDisplay](reference/androidx/navigation/NavDisplay).
  *
- * Information can be provided to the entire back stack via [NavContentWrapper.WrapBackStack] or to
- * a single record via [NavContentWrapper.WrapContent].
+ * Information can be provided to the entire back stack via [NavLocalProvider.ProvideToBackStack] or
+ * to a single entry via [NavLocalProvider.ProvideToEntry].
  */
-public interface NavContentWrapper {
+public interface NavLocalProvider {
 
     /**
-     * Allows a [NavContentWrapper] to execute on the entire backstack.
+     * Allows a [NavLocalProvider] to provide to the entire backstack.
      *
      * This function is called by the [NavWrapperManager] and should not be called directly.
      */
-    @Composable public fun WrapBackStack(backStack: List<Any>): Unit = Unit
+    @Composable public fun ProvideToBackStack(backStack: List<Any>): Unit = Unit
 
     /**
-     * Allows a [NavContentWrapper] to provide information to the content of a single record.
+     * Allows a [NavLocalProvider] to provide information to a single entry.
      *
      * This function is called by the [NavDisplay](reference/androidx/navigation/NavDisplay) and
      * should not be called directly.
      */
-    @Composable public fun <T : Any> WrapContent(record: NavRecord<T>)
+    @Composable public fun <T : Any> ProvideToEntry(entry: NavEntry<T>)
 }
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
index 4a137df..a994b16 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
@@ -19,56 +19,54 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 
-/** Creates a [NavContentWrapper]. */
+/** Creates a [NavLocalProvider]. */
 @Composable
-public fun rememberNavWrapperManager(
-    navContentWrappers: List<NavContentWrapper>
-): NavWrapperManager {
-    return remember { NavWrapperManager(navContentWrappers) }
+public fun rememberNavWrapperManager(navLocalProviders: List<NavLocalProvider>): NavWrapperManager {
+    return remember { NavWrapperManager(navLocalProviders) }
 }
 
 /**
- * Class that manages all of the provided [NavContentWrapper]. It is responsible for executing the
- * functions provided by each [NavContentWrapper] appropriately.
+ * Class that manages all of the provided [NavLocalProvider]. It is responsible for executing the
+ * functions provided by each [NavLocalProvider] appropriately.
  *
- * Note: the order in which the [NavContentWrapper]s are added to the list determines their scope,
- * i.e. a [NavContentWrapper] added earlier in a list has its data available to those added later.
+ * Note: the order in which the [NavLocalProvider]s are added to the list determines their scope,
+ * i.e. a [NavLocalProvider] added earlier in a list has its data available to those added later.
  *
- * @param navContentWrappers the [NavContentWrapper]s that are providing data to the content
+ * @param navLocalProviders the [NavLocalProvider]s that are providing data to the content
  */
-public class NavWrapperManager(navContentWrappers: List<NavContentWrapper> = emptyList()) {
+public class NavWrapperManager(navLocalProviders: List<NavLocalProvider> = emptyList()) {
     /**
-     * Final list of wrappers. This always adds a [SaveableStateNavContentWrapper] by default, as it
+     * Final list of wrappers. This always adds a [SaveableStateNavLocalProvider] by default, as it
      * is required. It then filters out any duplicates to ensure there is always one instance of any
      * wrapper at a given time.
      */
     private val finalWrappers =
-        (navContentWrappers + listOf(SaveableStateNavContentWrapper())).distinct()
+        (navLocalProviders + listOf(SaveableStateNavLocalProvider())).distinct()
 
     /**
-     * Calls the [NavContentWrapper.WrapBackStack] functions on each wrapper
+     * Calls the [NavLocalProvider.ProvideToBackStack] functions on each wrapper
      *
      * This function is called by the [NavDisplay](reference/androidx/navigation/NavDisplay) and
      * should not be called directly.
      */
     @Composable
     public fun PrepareBackStack(backStack: List<Any>) {
-        finalWrappers.distinct().forEach { it.WrapBackStack(backStack = backStack) }
+        finalWrappers.distinct().forEach { it.ProvideToBackStack(backStack = backStack) }
     }
 
     /**
-     * Calls the [NavContentWrapper.WrapContent] functions on each wrapper.
+     * Calls the [NavLocalProvider.ProvideToEntry] functions on each wrapper.
      *
      * This function is called by the [NavDisplay](reference/androidx/navigation/NavDisplay) and
      * should not be called directly.
      */
     @Composable
-    public fun <T : Any> ContentForRecord(record: NavRecord<T>) {
-        val key = record.key
+    public fun <T : Any> ContentForEntry(entry: NavEntry<T>) {
+        val key = entry.key
         finalWrappers
             .distinct()
-            .foldRight(record.content) { wrapper, contentLambda ->
-                { wrapper.WrapContent(NavRecord(key, record.featureMap, content = contentLambda)) }
+            .foldRight(entry.content) { wrapper, contentLambda ->
+                { wrapper.ProvideToEntry(NavEntry(key, entry.featureMap, content = contentLambda)) }
             }
             .invoke(key)
     }
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/RecordProvider.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/RecordProvider.kt
deleted file mode 100644
index 7f6dd14..0000000
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/RecordProvider.kt
+++ /dev/null
@@ -1,107 +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.navigation3
-
-import androidx.compose.runtime.Composable
-import kotlin.reflect.KClass
-
-@DslMarker public annotation class RecordDsl
-
-/** Creates an [RecordProviderBuilder] with the record providers provided in the builder. */
-public inline fun recordProvider(
-    noinline fallback: (unknownScreen: Any) -> NavRecord<*> = {
-        throw IllegalStateException("Unknown screen $it")
-    },
-    builder: RecordProviderBuilder.() -> Unit
-): (Any) -> NavRecord<*> = RecordProviderBuilder(fallback).apply(builder).build()
-
-/** DSL for constructing a new [NavRecord] */
-@Suppress("TopLevelBuilder")
-@RecordDsl
-public class RecordProviderBuilder(private val fallback: (unknownScreen: Any) -> NavRecord<*>) {
-    private val clazzProviders = mutableMapOf<KClass<*>, RecordClassProvider<*>>()
-    private val providers = mutableMapOf<Any, RecordProvider<*>>()
-
-    /** Builds a [NavRecord] for the given [key] that displays [content]. */
-    @Suppress("SetterReturnsThis", "MissingGetterMatchingBuilder")
-    public fun <T : Any> addRecordProvider(
-        key: T,
-        featureMap: Map<String, Any> = emptyMap(),
-        content: @Composable (T) -> Unit,
-    ) {
-        require(key !in providers) {
-            "A `record` with the key `key` has already been added: ${key}."
-        }
-        providers[key] = RecordProvider(key, featureMap, content)
-    }
-
-    /** Builds a [NavRecord] for the given [clazz] that displays [content]. */
-    @Suppress("SetterReturnsThis", "MissingGetterMatchingBuilder")
-    public fun <T : Any> addRecordProvider(
-        clazz: KClass<T>,
-        featureMap: Map<String, Any> = emptyMap(),
-        content: @Composable (T) -> Unit,
-    ) {
-        require(clazz !in clazzProviders) {
-            "A `record` with the same `clazz` has already been added: ${clazz.simpleName}."
-        }
-        clazzProviders[clazz] = RecordClassProvider(clazz, featureMap, content)
-    }
-
-    /**
-     * Returns an instance of recordProvider created from the record providers set on this builder.
-     */
-    @Suppress("UNCHECKED_CAST")
-    public fun build(): (Any) -> NavRecord<*> = { key ->
-        val recordClassProvider = clazzProviders[key::class] as? RecordClassProvider<Any>
-        val recordProvider = providers[key] as? RecordProvider<Any>
-        recordClassProvider?.run { NavRecord(key, featureMap, content) }
-            ?: recordProvider?.run { NavRecord(key, featureMap, content) }
-            ?: fallback.invoke(key)
-    }
-}
-
-/** Add an record provider to the [RecordProviderBuilder] */
-public fun <T : Any> RecordProviderBuilder.record(
-    key: T,
-    featureMap: Map<String, Any> = emptyMap(),
-    content: @Composable (T) -> Unit,
-) {
-    addRecordProvider(key, featureMap, content)
-}
-
-/** Add an record provider to the [RecordProviderBuilder] */
-public inline fun <reified T : Any> RecordProviderBuilder.record(
-    featureMap: Map<String, Any> = emptyMap(),
-    noinline content: @Composable (T) -> Unit,
-) {
-    addRecordProvider(T::class, featureMap, content)
-}
-
-/** Holds a Record class, featureMap, and content for that class */
-public data class RecordClassProvider<T : Any>(
-    val clazz: KClass<T>,
-    val featureMap: Map<String, Any>,
-    val content: @Composable (T) -> Unit,
-)
-
-/** Holds a Record class, featureMap, and content for that key */
-public data class RecordProvider<T : Any>(
-    val key: T,
-    val featureMap: Map<String, Any>,
-    val content: @Composable (T) -> Unit,
-)
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavLocalProvider.kt
similarity index 85%
rename from navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
rename to navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavLocalProvider.kt
index 9c6443f..69451da 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavLocalProvider.kt
@@ -24,19 +24,19 @@
 import androidx.compose.runtime.saveable.rememberSaveableStateHolder
 
 /**
- * Wraps the content of a [NavRecord] with a [SaveableStateHolder.SaveableStateProvider] to ensure
+ * Wraps the content of a [NavEntry] with a [SaveableStateHolder.SaveableStateProvider] to ensure
  * that calls to [rememberSaveable] within the content work properly and that state can be saved.
  *
- * This [NavContentWrapper] is the only one that is **required** as saving state is considered a
+ * This [NavLocalProvider] is the only one that is **required** as saving state is considered a
  * non-optional feature.
  */
-public class SaveableStateNavContentWrapper : NavContentWrapper {
+public class SaveableStateNavLocalProvider : NavLocalProvider {
     private var savedStateHolder: SaveableStateHolder? = null
     private val refCount: MutableObjectIntMap<Any> = MutableObjectIntMap()
     private var backstackSize = 0
 
     @Composable
-    override fun WrapBackStack(backStack: List<Any>) {
+    override fun ProvideToBackStack(backStack: List<Any>) {
         DisposableEffect(key1 = backStack) {
             refCount.clear()
             onDispose {}
@@ -56,7 +56,7 @@
                                 .getOrElse(key) {
                                     error(
                                         "Attempting to incorrectly dispose of backstack state in " +
-                                            "SaveableStateNavContentWrapper"
+                                            "SaveableStateNavLocalProvider"
                                     )
                                 }
                                 .minus(1)
@@ -67,8 +67,8 @@
     }
 
     @Composable
-    public override fun <T : Any> WrapContent(record: NavRecord<T>) {
-        val key = record.key
+    public override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
+        val key = entry.key
         DisposableEffect(key1 = key) {
             refCount[key] = refCount.getOrDefault(key, 0).plus(1)
             onDispose {
@@ -85,7 +85,7 @@
                             .getOrElse(key) {
                                 error(
                                     "Attempting to incorrectly dispose of state associated with " +
-                                        "key $key in SaveableStateNavContentWrapper"
+                                        "key $key in SaveableStateNavLocalProvider"
                                 )
                             }
                             .minus(1)
@@ -94,6 +94,6 @@
         }
 
         val id: Int = rememberSaveable(key) { key.hashCode() + backstackSize }
-        savedStateHolder?.SaveableStateProvider(id) { record.content.invoke(key) }
+        savedStateHolder?.SaveableStateProvider(id) { entry.content.invoke(key) }
     }
 }
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavContentWrapper.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavLocalProvider.kt
similarity index 83%
rename from navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavContentWrapper.kt
rename to navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavLocalProvider.kt
index c651c3d..56febae 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavContentWrapper.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavLocalProvider.kt
@@ -33,29 +33,29 @@
 import androidx.savedstate.savedState
 
 /**
- * Provides the content of a [NavRecord] with a [SavedStateRegistryOwner] and provides that
+ * Provides the content of a [NavEntry] with a [SavedStateRegistryOwner] and provides that
  * [SavedStateRegistryOwner] as a [LocalSavedStateRegistryOwner] so that it is available within the
  * content.
  */
-public object SavedStateNavContentWrapper : NavContentWrapper {
+public object SavedStateNavLocalProvider : NavLocalProvider {
 
     @Composable
-    override fun <T : Any> WrapContent(record: NavRecord<T>) {
-        val key = record.key
+    override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
+        val key = entry.key
         val childRegistry by
             rememberSaveable(
                 key,
                 stateSaver =
                     Saver(
                         save = { it.savedState },
-                        restore = { RecordSavedStateRegistry().apply { savedState = it } }
+                        restore = { EntrySavedStateRegistry().apply { savedState = it } }
                     )
             ) {
-                mutableStateOf(RecordSavedStateRegistry())
+                mutableStateOf(EntrySavedStateRegistry())
             }
 
         CompositionLocalProvider(LocalSavedStateRegistryOwner provides childRegistry) {
-            record.content.invoke(key)
+            entry.content.invoke(key)
         }
 
         DisposableEffect(key1 = key) {
@@ -70,7 +70,7 @@
     }
 }
 
-private class RecordSavedStateRegistry : SavedStateRegistryOwner {
+private class EntrySavedStateRegistry : SavedStateRegistryOwner {
     override val lifecycle: LifecycleRegistry = LifecycleRegistry(this)
     val savedStateRegistryController = SavedStateRegistryController.create(this)
     override val savedStateRegistry: SavedStateRegistry =
diff --git a/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordProviderTest.kt b/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/EntryProviderTest.kt
similarity index 64%
rename from navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordProviderTest.kt
rename to navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/EntryProviderTest.kt
index 67bb3e1..64e716e 100644
--- a/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordProviderTest.kt
+++ b/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/EntryProviderTest.kt
@@ -20,40 +20,40 @@
 import kotlin.test.Test
 import kotlin.test.fail
 
-class RecordProviderTest {
+class EntryProviderTest {
 
     @Test
-    fun recordProvider_withUniqueInitializers_returnsRecords() {
-        val provider = recordProvider {
-            record("first") {}
-            record("second") {}
+    fun entryProvider_withUniqueInitializers_returnsEntries() {
+        val provider = entryProvider {
+            entry("first") {}
+            entry("second") {}
         }
 
-        val record1 = provider.invoke("first")
-        val record2 = provider.invoke("second")
+        val entry1 = provider.invoke("first")
+        val entry2 = provider.invoke("second")
 
-        assertThat(record1.key).isEqualTo("first")
-        assertThat(record2.key).isEqualTo("second")
+        assertThat(entry1.key).isEqualTo("first")
+        assertThat(entry2.key).isEqualTo("second")
     }
 
     @Test
-    fun recordProvider_withDuplicatedInitializers_throwsException() {
+    fun entryProvider_withDuplicatedInitializers_throwsException() {
         try {
-            recordProvider {
-                record("first") {}
-                record("first") {}
+            entryProvider {
+                entry("first") {}
+                entry("first") {}
             }
             fail("Expected `IllegalArgumentException` but no exception has been throw.")
         } catch (e: IllegalArgumentException) {
             assertThat(e)
                 .hasMessageThat()
-                .isEqualTo("A `record` with the key `key` has already been added: first.")
+                .isEqualTo("An `entry` with the key `key` has already been added: first.")
         }
     }
 
     @Test
-    fun recordProvider_noInitializers_getsInvalidRecord() {
-        val provider = recordProvider {}
+    fun entryProvider_noInitializers_getsInvalidEntry() {
+        val provider = entryProvider {}
         try {
             provider.invoke("something")
             fail("Expected `IllegalStateException` but no exception has been throw.")
diff --git a/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordTest.kt b/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordTest.kt
index b049d3f..863305e 100644
--- a/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordTest.kt
+++ b/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordTest.kt
@@ -19,24 +19,24 @@
 import androidx.kruth.assertThat
 import kotlin.test.Test
 
-class RecordTest {
+class EntryTest {
 
     @Test
     fun getKey() {
-        val record = NavRecord(key = "myKey", content = {})
-        assertThat(record.key).isEqualTo("myKey")
+        val entry = NavEntry(key = "myKey", content = {})
+        assertThat(entry.key).isEqualTo("myKey")
     }
 
     @Test
     fun getFeatureMap() {
-        val record =
-            NavRecord(
+        val entry =
+            NavEntry(
                 key = "myKey",
                 featureMap = mapOf("feature1" to 1, "feature2" to MyObject),
                 content = {}
             )
-        assertThat(record.featureMap["feature1"]).isEqualTo(1)
-        assertThat(record.featureMap["feature2"]).isEqualTo(MyObject)
+        assertThat(entry.featureMap["feature1"]).isEqualTo(1)
+        assertThat(entry.featureMap["feature2"]).isEqualTo(MyObject)
     }
 
     object MyObject
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index 3d44721..1dc4afe 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -853,9 +853,9 @@
     private fun resetViewsAndModels(fileUri: Uri) {
         if (pdfLoader != null) {
             pdfLoaderCallbacks?.uri = fileUri
-            paginatedView?.resetModels()
             destroyContentModel()
         }
+        paginatedView?.resetModels()
         fastScrollView?.resetContents()
         findInFileView?.resetFindInFile()
     }
diff --git a/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt b/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
index ec8233d..19b75a2 100644
--- a/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
+++ b/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
@@ -52,7 +52,6 @@
 import androidx.pdf.viewer.fragment.search.PdfSearchViewManager
 import androidx.pdf.viewer.fragment.util.getCenter
 import androidx.pdf.viewer.fragment.view.PdfViewManager
-import com.google.android.material.color.MaterialColors
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 
@@ -202,19 +201,9 @@
         pdfViewManager =
             PdfViewManager(
                 pdfView = pdfView,
-                // TODO(b/385684706): Update colors for highlights
                 selectedHighlightColor =
-                    MaterialColors.getColor(
-                        pdfView,
-                        com.google.android.material.R.attr.colorPrimaryFixed,
-                        requireContext().getColor(R.color.selected_highlight_color)
-                    ),
-                highlightColor =
-                    MaterialColors.getColor(
-                        pdfView,
-                        com.google.android.material.R.attr.colorSecondaryFixedDim,
-                        requireContext().getColor(R.color.highlight_color)
-                    )
+                    requireContext().getColor(R.color.selected_highlight_color),
+                highlightColor = requireContext().getColor(R.color.highlight_color)
             )
         pdfSearchViewManager = PdfSearchViewManager(pdfSearchView)
 
diff --git a/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/build.gradle b/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/build.gradle
index 5aed355..88365c64 100644
--- a/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/build.gradle
+++ b/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/build.gradle
@@ -28,6 +28,10 @@
     minSdk = 21
     buildToolsVersion = AndroidXConfig.getDefaultAndroidConfig(project).buildToolsVersion
 
+    //TODO(b/389890488): This is added to suppress missing stub classes warning from ui-compose.
+    //Can be removed once linked bug is fixed.
+    optimization.keepRules.files += project.file('proguard-rules.pro')
+
     bundle {
         packageName = "androidx.privacysandbox.ui.integration.mediateesdkproviderwrapper"
 
diff --git a/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/proguard-rules.pro b/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/proguard-rules.pro
new file mode 100644
index 0000000..13de6f6
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/proguard-rules.pro
@@ -0,0 +1,13 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+-dontwarn android.view.RenderNode
+-dontwarn android.view.DisplayListCanvas
+-dontwarn android.view.HardwareCanvas
diff --git a/privacysandbox/ui/integration-tests/testapp/build.gradle b/privacysandbox/ui/integration-tests/testapp/build.gradle
index 7ec0127..a9a297b 100644
--- a/privacysandbox/ui/integration-tests/testapp/build.gradle
+++ b/privacysandbox/ui/integration-tests/testapp/build.gradle
@@ -16,9 +16,9 @@
 
 plugins {
     id("AndroidXPlugin")
+    id("AndroidXComposePlugin")
     id("com.android.application")
     id("org.jetbrains.kotlin.android")
-    id("AndroidXComposePlugin")
 }
 
 android {
@@ -45,6 +45,7 @@
 dependencies {
     implementation(libs.kotlinStdlib)
     api("androidx.annotation:annotation:1.8.1")
+
     implementation("androidx.core:core-ktx:1.8.0")
     implementation("androidx.appcompat:appcompat:1.6.0")
     implementation("com.google.android.material:material:1.6.0")
@@ -52,9 +53,7 @@
     implementation("androidx.drawerlayout:drawerlayout:1.2.0")
     implementation("androidx.constraintlayout:constraintlayout:2.0.1")
     implementation("androidx.recyclerview:recyclerview:1.3.2")
-    implementation("androidx.compose.foundation:foundation-layout:1.7.5")
-    implementation("androidx.compose.material3:material3-android:1.3.1")
-    implementation("androidx.compose.ui:ui-util:1.7.5")
+    implementation("androidx.compose.material3:material3:1.3.1")
     implementation(project(":privacysandbox:activity:activity-core"))
     implementation(project(":privacysandbox:activity:activity-client"))
     implementation(project(":privacysandbox:sdkruntime:sdkruntime-client"))
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 56e84a8..3559d29 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -23,6 +23,9 @@
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.fragment.app.Fragment
 import androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat
 import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
@@ -46,6 +49,7 @@
 
     private lateinit var sdkSandboxManager: SdkSandboxManagerCompat
     private lateinit var activity: Activity
+    protected var providerUiOnTop by mutableStateOf(true)
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -117,9 +121,8 @@
     }
 
     open fun handleDrawerStateChange(isDrawerOpen: Boolean) {
-        getSandboxedSdkViews().forEach {
-            it.orderProviderUiAboveClientUi(!isDrawerOpen && isZOrderOnTop)
-        }
+        providerUiOnTop = !isDrawerOpen && !isZOrderBelowToggleChecked
+        getSandboxedSdkViews().forEach { it.orderProviderUiAboveClientUi(providerUiOnTop) }
     }
 
     private inner class TestEventListener(val view: SandboxedSdkView) :
@@ -147,7 +150,7 @@
         private const val MEDIATEE_SDK_NAME =
             "androidx.privacysandbox.ui.integration.mediateesdkproviderwrapper"
         const val TAG = "TestSandboxClient"
-        var isZOrderOnTop = true
+        var isZOrderBelowToggleChecked = false
         @AdType var currentAdType = AdType.BASIC_NON_WEBVIEW
         @MediationOption var currentMediationOption = MediationOption.NON_MEDIATED
         var shouldDrawViewabilityLayer = false
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index 8ba632c..8467ece 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -237,7 +237,7 @@
 
     private fun initializeZOrderToggleButton() {
         zOrderToggleButton.setOnCheckedChangeListener { _, isChecked ->
-            BaseFragment.isZOrderOnTop = !isChecked
+            BaseFragment.isZOrderBelowToggleChecked = isChecked
         }
     }
 
@@ -290,8 +290,24 @@
 
     private fun selectCuj(menuItem: MenuItem) {
         when (menuItem.itemId) {
-            R.id.item_resize -> switchContentFragment(ResizeFragment(), menuItem.title)
-            R.id.item_scroll -> switchContentFragment(ScrollFragment(), menuItem.title)
+            R.id.item_resize ->
+                if (useCompose) {
+                    switchContentFragment(
+                        ResizeComposeFragment(),
+                        "${menuItem.title} ${getString(R.string.compose)}"
+                    )
+                } else {
+                    switchContentFragment(ResizeFragment(), menuItem.title)
+                }
+            R.id.item_scroll ->
+                if (useCompose) {
+                    switchContentFragment(
+                        ScrollComposeFragment(),
+                        "${menuItem.title} ${getString(R.string.compose)}"
+                    )
+                } else {
+                    switchContentFragment(ScrollFragment(), menuItem.title)
+                }
             R.id.item_pooling_container ->
                 switchContentFragment(PoolingContainerFragment(), menuItem.title)
             R.id.item_fullscreen ->
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
index abdf0d4..fd5d6e9 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
@@ -38,7 +38,7 @@
 
     override fun handleDrawerStateChange(isDrawerOpen: Boolean) {
         super.handleDrawerStateChange(isDrawerOpen)
-        (recyclerView.adapter as CustomAdapter).zOrderOnTop = !isDrawerOpen && isZOrderOnTop
+        (recyclerView.adapter as CustomAdapter).zOrderOnTop = providerUiOnTop
     }
 
     override fun handleLoadAdFromDrawer(
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeComposeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeComposeFragment.kt
new file mode 100644
index 0000000..c537976
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeComposeFragment.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.privacysandbox.ui.integration.testapp
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUi
+import androidx.privacysandbox.ui.client.view.SandboxedSdkViewEventListener
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+class ResizeComposeFragment : BaseFragment() {
+
+    private var adapter: SandboxedUiAdapter? by mutableStateOf(null)
+    private var adEventText by mutableStateOf("")
+    private var bannerDimension by mutableStateOf(BannerDimension())
+    private val onBannerDimensionChanged: (BannerDimension) -> Unit = { currentDimension ->
+        val displayMetrics = resources.displayMetrics
+        val maxSizePixels = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels)
+        val newSize = { currentSize: Int, maxSize: Int ->
+            (currentSize + (100..200).random()) % maxSize
+        }
+        val newWidth = newSize(currentDimension.width.value.toInt(), maxSizePixels).dp
+        val newHeight = newSize(currentDimension.height.value.toInt(), maxSizePixels).dp
+        bannerDimension = BannerDimension(newWidth, newHeight)
+    }
+
+    override fun handleLoadAdFromDrawer(
+        adType: Int,
+        mediationOption: Int,
+        drawViewabilityLayer: Boolean
+    ) {
+        currentAdType = adType
+        currentMediationOption = mediationOption
+        shouldDrawViewabilityLayer = drawViewabilityLayer
+        setAdAdapter()
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        setAdAdapter()
+        return ComposeView(requireContext()).apply {
+            // Dispose of the Composition when the view's LifecycleOwner is destroyed
+            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+            setContent { ResizeableBannerAd(adapter, bannerDimension, onBannerDimensionChanged) }
+        }
+    }
+
+    @Composable
+    fun ResizeableBannerAd(
+        adapter: SandboxedUiAdapter?,
+        bannerDimension: BannerDimension,
+        onResizeClicked: (BannerDimension) -> Unit
+    ) {
+        Column(
+            modifier = Modifier.fillMaxSize().padding(16.dp),
+            verticalArrangement = Arrangement.Top,
+            horizontalAlignment = Alignment.Start
+        ) {
+            val sandboxedSdkUiModifier =
+                if (bannerDimension.height != 0.dp && bannerDimension.width != 0.dp) {
+                    Modifier.width(bannerDimension.width)
+                        .weight(
+                            1f,
+                        )
+                } else {
+                    Modifier.fillMaxWidth().weight(1f)
+                }
+
+            Text("Ad state: $adEventText")
+            if (adapter != null) {
+                SandboxedSdkUi(
+                    adapter,
+                    sandboxedSdkUiModifier,
+                    providerUiOnTop = providerUiOnTop,
+                    sandboxedSdkViewEventListener =
+                        object : SandboxedSdkViewEventListener {
+                            override fun onUiDisplayed() {
+                                adEventText = "Ad is visible"
+                            }
+
+                            override fun onUiError(error: Throwable) {
+                                adEventText = "Error loading ad : ${error.message}"
+                            }
+
+                            override fun onUiClosed() {
+                                adEventText = "Ad session is closed"
+                            }
+                        },
+                )
+            }
+            Button(onClick = { onResizeClicked(bannerDimension) }) { Text("Resize") }
+        }
+    }
+
+    private fun setAdAdapter() {
+        val coroutineScope = MainScope()
+        coroutineScope.launch {
+            adapter =
+                SandboxedUiAdapterFactory.createFromCoreLibInfo(
+                    getSdkApi()
+                        .loadBannerAd(
+                            currentAdType,
+                            currentMediationOption,
+                            false,
+                            shouldDrawViewabilityLayer,
+                        )
+                )
+        }
+    }
+
+    data class BannerDimension(val width: Dp = 0.dp, val height: Dp = 0.dp)
+}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt
new file mode 100644
index 0000000..6acfb9c
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.integration.testapp
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUi
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+class ScrollComposeFragment : BaseFragment() {
+
+    private var bottomBannerAdapter: SandboxedUiAdapter? by mutableStateOf(null)
+    private var scrollBannerAdapter: SandboxedUiAdapter? by mutableStateOf(null)
+
+    override fun handleLoadAdFromDrawer(
+        adType: Int,
+        mediationOption: Int,
+        drawViewabilityLayer: Boolean
+    ) {
+        currentAdType = adType
+        currentMediationOption = mediationOption
+        shouldDrawViewabilityLayer = drawViewabilityLayer
+        setAdapter()
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        setAdapter()
+        return ComposeView(requireContext()).apply {
+            // Dispose of the Composition when the view's LifecycleOwner is destroyed
+            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+            setContent {
+                Column(
+                    modifier = Modifier.fillMaxSize().padding(16.dp),
+                    verticalArrangement = Arrangement.Top,
+                    horizontalAlignment = Alignment.Start
+                ) {
+                    Column(modifier = Modifier.weight(0.8f).verticalScroll(rememberScrollState())) {
+                        scrollBannerAdapter?.let {
+                            SandboxedSdkUi(
+                                it,
+                                Modifier.fillMaxWidth().height(200.dp),
+                                providerUiOnTop = providerUiOnTop
+                            )
+                        }
+                        Text(stringResource(R.string.long_text), Modifier.padding(vertical = 16.dp))
+                    }
+                    bottomBannerAdapter?.let {
+                        SandboxedSdkUi(it, Modifier.weight(0.2f), providerUiOnTop = providerUiOnTop)
+                    }
+                }
+            }
+        }
+    }
+
+    private fun setAdapter() {
+        val coroutineScope = MainScope()
+        coroutineScope.launch {
+            bottomBannerAdapter =
+                SandboxedUiAdapterFactory.createFromCoreLibInfo(
+                    getSdkApi()
+                        .loadBannerAd(
+                            currentAdType,
+                            currentMediationOption,
+                            false,
+                            shouldDrawViewabilityLayer,
+                        )
+                )
+            scrollBannerAdapter =
+                SandboxedUiAdapterFactory.createFromCoreLibInfo(
+                    getSdkApi()
+                        .loadBannerAd(
+                            currentAdType,
+                            currentMediationOption,
+                            false,
+                            shouldDrawViewabilityLayer,
+                        )
+                )
+        }
+    }
+}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
index cfdbb065..143c337 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
@@ -16,6 +16,7 @@
 
 <resources>
     <string name="resize_cuj">Resize CUJ</string>
+    <string name="compose">Compose</string>
     <string name="scroll_cuj">Scroll CUJ</string>
     <string name="poolingcontainer_cuj">PoolingContainer CUJ</string>
     <string name="fullscreen_cuj">Fullscreen CUJ</string>
diff --git a/privacysandbox/ui/integration-tests/testsdkproviderwrapper/build.gradle b/privacysandbox/ui/integration-tests/testsdkproviderwrapper/build.gradle
index 96d56fa..0532e9c 100644
--- a/privacysandbox/ui/integration-tests/testsdkproviderwrapper/build.gradle
+++ b/privacysandbox/ui/integration-tests/testsdkproviderwrapper/build.gradle
@@ -28,6 +28,10 @@
     minSdk = 21
     buildToolsVersion = AndroidXConfig.getDefaultAndroidConfig(project).buildToolsVersion
 
+    //TODO(b/389890488): This is added to suppress missing stub classes warning from ui-compose.
+    //Can be removed once linked bug is fixed.
+    optimization.keepRules.files += project.file('proguard-rules.pro')
+
     bundle {
         packageName = "androidx.privacysandbox.ui.integration.testsdkproviderwrapper"
 
diff --git a/privacysandbox/ui/integration-tests/testsdkproviderwrapper/proguard-rules.pro b/privacysandbox/ui/integration-tests/testsdkproviderwrapper/proguard-rules.pro
new file mode 100644
index 0000000..13de6f6
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testsdkproviderwrapper/proguard-rules.pro
@@ -0,0 +1,13 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+-dontwarn android.view.RenderNode
+-dontwarn android.view.DisplayListCanvas
+-dontwarn android.view.HardwareCanvas
diff --git a/privacysandbox/ui/ui-client/api/current.txt b/privacysandbox/ui/ui-client/api/current.txt
index a3d0e4d..94a15f0 100644
--- a/privacysandbox/ui/ui-client/api/current.txt
+++ b/privacysandbox/ui/ui-client/api/current.txt
@@ -10,6 +10,10 @@
 
 package androidx.privacysandbox.ui.client.view {
 
+  public final class SandboxedSdkUiKt {
+    method @androidx.compose.runtime.Composable public static void SandboxedSdkUi(androidx.privacysandbox.ui.core.SandboxedUiAdapter sandboxedUiAdapter, optional androidx.compose.ui.Modifier modifier, optional boolean providerUiOnTop, optional androidx.privacysandbox.ui.client.view.SandboxedSdkViewEventListener? sandboxedSdkViewEventListener);
+  }
+
   public final class SandboxedSdkView extends android.view.ViewGroup {
     ctor public SandboxedSdkView(android.content.Context context);
     ctor public SandboxedSdkView(android.content.Context context, optional android.util.AttributeSet? attrs);
diff --git a/privacysandbox/ui/ui-client/api/restricted_current.txt b/privacysandbox/ui/ui-client/api/restricted_current.txt
index a3d0e4d..94a15f0 100644
--- a/privacysandbox/ui/ui-client/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-client/api/restricted_current.txt
@@ -10,6 +10,10 @@
 
 package androidx.privacysandbox.ui.client.view {
 
+  public final class SandboxedSdkUiKt {
+    method @androidx.compose.runtime.Composable public static void SandboxedSdkUi(androidx.privacysandbox.ui.core.SandboxedUiAdapter sandboxedUiAdapter, optional androidx.compose.ui.Modifier modifier, optional boolean providerUiOnTop, optional androidx.privacysandbox.ui.client.view.SandboxedSdkViewEventListener? sandboxedSdkViewEventListener);
+  }
+
   public final class SandboxedSdkView extends android.view.ViewGroup {
     ctor public SandboxedSdkView(android.content.Context context);
     ctor public SandboxedSdkView(android.content.Context context, optional android.util.AttributeSet? attrs);
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index bec5091..7546329 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -27,12 +27,14 @@
     id("AndroidXPlugin")
     id("com.android.library")
     id("org.jetbrains.kotlin.android")
+    id("AndroidXComposePlugin")
 }
 
 dependencies {
     api(libs.kotlinStdlib)
     api("androidx.annotation:annotation:1.8.1")
     api("androidx.core:core:1.12.0")
+    api("androidx.compose.ui:ui:1.7.6")
     api("androidx.lifecycle:lifecycle-common:2.6.2")
     api("androidx.customview:customview-poolingcontainer:1.0.0")
     api(project(":privacysandbox:ui:ui-core"))
@@ -52,6 +54,9 @@
     androidTestImplementation(project(":appcompat:appcompat"))
     androidTestImplementation(project(":privacysandbox:ui:ui-provider"))
     androidTestImplementation(project(":privacysandbox:ui:integration-tests:testingutils"))
+    androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.7.6")
+    androidTestImplementation("androidx.compose.foundation:foundation-layout:1.7.6")
+    debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.6")
 }
 
 android {
diff --git a/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml b/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
index 1f5dbd3..c245d56 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
+++ b/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   Copyright 2022 The Android Open Source Project
 
   Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,18 +16,24 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
     <!-- This override is okay because the associated tests only run on T+ -->
-    <uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator, androidx.test.uiautomator" />
-    <application android:supportsRtl="true"
+    <application
+        android:supportsRtl="true"
         android:theme="@style/Theme.AppCompat">
-    <activity
-        android:name="androidx.privacysandbox.ui.client.test.UiLibActivity"
-        android:configChanges="orientation|screenSize"
-        android:exported="true"/>
-    <activity android:name=".SecondActivity"
-        android:exported="true"/>
+        <activity
+            android:name="androidx.privacysandbox.ui.client.test.UiLibActivity"
+            android:configChanges="orientation|screenSize"
+            android:exported="true" />
+        <activity
+            android:name="androidx.privacysandbox.ui.client.test.SecondActivity"
+            android:exported="true" />
+        <activity
+            android:name="androidx.privacysandbox.ui.client.test.UiLibComposeActivity"
+            android:configChanges="orientation|screenSize"
+            android:exported="true" />
     </application>
-
     <queries>
         <package android:name="androidx.privacysandbox.ui.client.test" />
     </queries>
+
+    <uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator, androidx.test.uiautomator" />
 </manifest>
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/FailingTestSandboxedUiAdapter.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/FailingTestSandboxedUiAdapter.kt
new file mode 100644
index 0000000..79dd0c8
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/FailingTestSandboxedUiAdapter.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.privacysandbox.ui.client.test
+
+import android.content.Context
+import android.os.IBinder
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.provider.AbstractSandboxedUiAdapter
+import java.util.concurrent.Executor
+
+class FailingTestSandboxedUiAdapter : AbstractSandboxedUiAdapter() {
+    override fun openSession(
+        context: Context,
+        windowInputToken: IBinder,
+        initialWidth: Int,
+        initialHeight: Int,
+        isZOrderOnTop: Boolean,
+        clientExecutor: Executor,
+        client: SandboxedUiAdapter.SessionClient
+    ) {
+        clientExecutor.execute { client.onSessionError(Exception("Error in openSession()")) }
+    }
+}
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkUiTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkUiTest.kt
new file mode 100644
index 0000000..e057517
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkUiTest.kt
@@ -0,0 +1,558 @@
+/*
+ * 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.privacysandbox.ui.client.test
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.util.DisplayMetrics
+import android.util.TypedValue
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.privacysandbox.ui.client.test.SandboxedSdkViewTest.Companion.SHORTEST_TIME_BETWEEN_SIGNALS_MS
+import androidx.privacysandbox.ui.client.test.SandboxedSdkViewTest.Companion.TIMEOUT
+import androidx.privacysandbox.ui.client.test.SandboxedSdkViewTest.Companion.UI_INTENSIVE_TIMEOUT
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUi
+import androidx.privacysandbox.ui.client.view.SandboxedSdkView
+import androidx.privacysandbox.ui.integration.testingutils.TestEventListener
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import org.hamcrest.Matchers.instanceOf
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO(b/374919355): Create a common test framework for testing Compose and View UI lib constructs
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class SandboxedSdkUiTest {
+    @get:Rule val composeTestRule = createAndroidComposeRule<UiLibComposeActivity>()
+    private var testSandboxedUiAdapter by mutableStateOf(TestSandboxedUiAdapter())
+    private var eventListener by mutableStateOf(TestEventListener())
+    private var providerUiOnTop by mutableStateOf(true)
+    private var size by mutableStateOf(20.dp)
+    private lateinit var uiDevice: UiDevice
+
+    @Before
+    fun setup() {
+        testSandboxedUiAdapter = TestSandboxedUiAdapter()
+        eventListener = TestEventListener()
+        providerUiOnTop = true
+        uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+    }
+
+    @Test
+    fun eventListenerErrorTest() {
+        composeTestRule.setContent {
+            SandboxedSdkUi(
+                sandboxedUiAdapter = FailingTestSandboxedUiAdapter(),
+                modifier = Modifier,
+                providerUiOnTop = providerUiOnTop,
+                sandboxedSdkViewEventListener = eventListener
+            )
+        }
+        assertThat(eventListener.errorLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(eventListener.error?.message).isEqualTo("Error in openSession()")
+    }
+
+    @Test
+    fun addEventListenerTest() {
+        // Initially no events are received when the session is not open.
+        assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+        // When session is open, the events are received
+        addNodeToLayout()
+        assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    @Test
+    fun sessionNotOpenedWhenWindowIsNotVisibleTest() {
+        // the window is not visible when the activity is in the CREATED state.
+        composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionNotOpened()
+        // the window becomes visible when the activity is in the STARTED state.
+        composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.STARTED)
+        testSandboxedUiAdapter.assertSessionOpened()
+    }
+
+    @Test
+    fun onAttachedToWindowTest() {
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionOpened()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(hasChildCount(1)))
+            .check { view, exception ->
+                if (
+                    view.layoutParams.width != WRAP_CONTENT ||
+                        view.layoutParams.height != WRAP_CONTENT
+                ) {
+                    throw exception
+                }
+            }
+    }
+
+    @Test
+    fun childViewRemovedOnErrorTest() {
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionOpened()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check(matches(hasChildCount(1)))
+        composeTestRule.activityRule.withActivity {
+            testSandboxedUiAdapter.internalClient!!.onSessionError(Exception())
+        }
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check(matches(hasChildCount(0)))
+    }
+
+    @Test
+    fun onZOrderChangedTest() {
+        addNodeToLayout()
+        // When session is opened, the provider should not receive a Z-order notification.
+        testSandboxedUiAdapter.assertSessionOpened()
+        val session = testSandboxedUiAdapter.testSession
+        assertThat(session?.zOrderChangedLatch?.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+        assertThat(testSandboxedUiAdapter.isZOrderOnTop).isTrue()
+        // When state changes to false, the provider should be notified.
+        providerUiOnTop = false
+        composeTestRule.waitForIdle()
+        assertThat(session?.zOrderChangedLatch?.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(testSandboxedUiAdapter.isZOrderOnTop).isFalse()
+        // When state changes back to true, the provider should be notified.
+        session?.zOrderChangedLatch = CountDownLatch(1)
+        providerUiOnTop = true
+        composeTestRule.waitForIdle()
+        assertThat(session?.zOrderChangedLatch?.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(testSandboxedUiAdapter.isZOrderOnTop).isTrue()
+    }
+
+    @Test
+    fun setZOrderNotOnTopBeforeOpeningSessionTest() {
+        providerUiOnTop = false
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionOpened()
+        val session = testSandboxedUiAdapter.testSession
+        // The initial Z-order state is passed to the session, but notifyZOrderChanged is not called
+        assertThat(session?.zOrderChangedLatch?.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+        assertThat(testSandboxedUiAdapter.isZOrderOnTop).isFalse()
+    }
+
+    @Test
+    fun setZOrderNotOnTopWhileSessionLoadingTest() {
+        testSandboxedUiAdapter.delayOpenSessionCallback = true
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionOpened()
+        providerUiOnTop = false
+        composeTestRule.waitForIdle()
+        val session = testSandboxedUiAdapter.testSession!!
+        assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+        composeTestRule.activityRule.withActivity { testSandboxedUiAdapter.sendOnSessionOpened() }
+        // After session has opened, the pending Z order changed made while loading is notified
+        // to the session.
+        assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(testSandboxedUiAdapter.isZOrderOnTop).isFalse()
+    }
+
+    @Test
+    fun onConfigurationChangedSessionRemainsOpened() {
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionOpened()
+        // newWindow() will be triggered by a window state change, even if the activity handles
+        // orientation changes without recreating the activity.
+        uiDevice.performActionAndWait(
+            { uiDevice.setOrientationLeft() },
+            Until.newWindow(),
+            UI_INTENSIVE_TIMEOUT
+        )
+        testSandboxedUiAdapter.assertSessionNotClosed()
+        uiDevice.performActionAndWait(
+            { uiDevice.setOrientationNatural() },
+            Until.newWindow(),
+            UI_INTENSIVE_TIMEOUT
+        )
+        testSandboxedUiAdapter.assertSessionNotClosed()
+    }
+
+    @Test
+    fun onConfigurationChangedTestSameConfigurationTest() {
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionOpened()
+        composeTestRule.activityRule.withActivity {
+            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+        }
+        assertThat(testSandboxedUiAdapter.wasOnConfigChangedCalled()).isFalse()
+    }
+
+    @Test
+    fun onPaddingSetTest() {
+        var padding by mutableStateOf(0.dp)
+        composeTestRule.setContent {
+            SandboxedSdkUi(
+                sandboxedUiAdapter = testSandboxedUiAdapter,
+                modifier = Modifier.padding(all = padding),
+                providerUiOnTop = providerUiOnTop,
+                sandboxedSdkViewEventListener = eventListener
+            )
+        }
+        testSandboxedUiAdapter.assertSessionOpened()
+        padding = 10.dp
+        composeTestRule.waitForIdle()
+        assertThat(testSandboxedUiAdapter.wasNotifyResizedCalled()).isTrue()
+    }
+
+    @Test
+    fun signalsSentWhenPaddingApplied() {
+        var padding by mutableStateOf(0.dp)
+        composeTestRule.setContent {
+            SandboxedSdkUi(
+                sandboxedUiAdapter = testSandboxedUiAdapter,
+                modifier = Modifier.padding(all = padding),
+                providerUiOnTop = providerUiOnTop,
+                sandboxedSdkViewEventListener = eventListener
+            )
+        }
+        testSandboxedUiAdapter.assertSessionOpened()
+        val session = testSandboxedUiAdapter.testSession
+        session?.runAndRetrieveNextUiChange {
+            padding = 10.dp
+            composeTestRule.waitForIdle()
+        }
+        assertThat(session?.shortestGapBetweenUiChangeEvents)
+            .isAtLeast(SHORTEST_TIME_BETWEEN_SIGNALS_MS)
+    }
+
+    @Test
+    fun onLayoutTestWithSizeChangeTest() {
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionOpened()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(hasChildCount(1)))
+            .check { view, exception ->
+                val expectedSize = size.toPx(view.context.resources.displayMetrics)
+                if (view.width != expectedSize || view.height != expectedSize) {
+                    throw exception
+                }
+            }
+        size = 30.dp
+        composeTestRule.waitForIdle()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java))
+            .check(matches(isDisplayed()))
+            .check(matches(hasChildCount(1)))
+            .check { view, exception ->
+                val expectedSize = size.toPx(view.context.resources.displayMetrics)
+                if (view.width != expectedSize || view.height != expectedSize) {
+                    throw exception
+                }
+            }
+        assertThat(testSandboxedUiAdapter.wasNotifyResizedCalled()).isTrue()
+    }
+
+    @Test
+    fun onLayoutTestNoSizeChangeTest() {
+        size = 20.dp
+        addNodeToLayout()
+        testSandboxedUiAdapter.assertSessionOpened()
+        size = 20.dp
+        composeTestRule.waitForIdle()
+        assertThat(testSandboxedUiAdapter.wasNotifyResizedCalled()).isFalse()
+    }
+
+    @Test
+    fun onLayoutTestViewShiftWithoutSizeChangeTest() {
+        var offset by mutableStateOf(0.dp)
+        composeTestRule.setContent {
+            SandboxedSdkUi(
+                sandboxedUiAdapter = testSandboxedUiAdapter,
+                modifier = Modifier.offset(offset),
+                providerUiOnTop = providerUiOnTop,
+                sandboxedSdkViewEventListener = eventListener
+            )
+        }
+        testSandboxedUiAdapter.assertSessionOpened()
+        offset = 10.dp
+        composeTestRule.waitForIdle()
+        assertThat(testSandboxedUiAdapter.wasNotifyResizedCalled()).isFalse()
+    }
+
+    @Test
+    fun onSdkRequestsResizeTest() {
+        val boxSize = 300.dp
+        composeTestRule.setContent {
+            Box(modifier = Modifier.requiredSize(boxSize)) {
+                SandboxedSdkUi(
+                    sandboxedUiAdapter = testSandboxedUiAdapter,
+                    modifier = Modifier,
+                    providerUiOnTop = providerUiOnTop,
+                    sandboxedSdkViewEventListener = eventListener
+                )
+            }
+        }
+        testSandboxedUiAdapter.assertSessionOpened()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+            assertThat(view.width).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+            assertThat(view.height).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+        }
+        composeTestRule.activityRule.withActivity {
+            testSandboxedUiAdapter.testSession?.requestResize(200, 200) as Any
+        }
+        composeTestRule.waitForIdle()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+            assertThat(view.width).isEqualTo(200)
+            assertThat(view.height).isEqualTo(200)
+        }
+    }
+
+    @Test
+    fun requestResizeWithMeasureSpecAtMostExceedsParentBoundsTest() {
+        val boxSize = 300.dp
+        composeTestRule.setContent {
+            Box(modifier = Modifier.requiredSize(boxSize)) {
+                SandboxedSdkUi(
+                    sandboxedUiAdapter = testSandboxedUiAdapter,
+                    modifier = Modifier,
+                    providerUiOnTop = providerUiOnTop,
+                    sandboxedSdkViewEventListener = eventListener
+                )
+            }
+        }
+        testSandboxedUiAdapter.assertSessionOpened()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+            assertThat(view.width).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+            assertThat(view.height).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+        }
+        composeTestRule.activityRule.withActivity {
+            val newSize = (boxSize + 10.dp).toPx(resources.displayMetrics)
+            testSandboxedUiAdapter.testSession?.requestResize(newSize, newSize) as Any
+        }
+        composeTestRule.waitForIdle()
+        // the resize is constrained by the parent's size
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+            assertThat(view.width).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+            assertThat(view.height).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+        }
+    }
+
+    @Test
+    fun requestResizeWithMeasureSpecExactlyTest() {
+        var boxWidth = 0.dp
+        var boxHeight = 0.dp
+        composeTestRule.setContent {
+            // Get local density from composable
+            val localDensity = LocalDensity.current
+            Box(
+                modifier =
+                    Modifier.fillMaxSize().onGloballyPositioned { coordinates ->
+                        boxWidth = with(localDensity) { coordinates.size.width.toDp() }
+                        boxHeight = with(localDensity) { coordinates.size.height.toDp() }
+                    }
+            ) {
+                SandboxedSdkUi(
+                    sandboxedUiAdapter = testSandboxedUiAdapter,
+                    modifier = Modifier.fillMaxSize(),
+                    providerUiOnTop = providerUiOnTop,
+                    sandboxedSdkViewEventListener = eventListener
+                )
+            }
+        }
+        testSandboxedUiAdapter.assertSessionOpened()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+            assertThat(view.width).isEqualTo(boxWidth.toPx(view.context.resources.displayMetrics))
+            assertThat(view.height).isEqualTo(boxHeight.toPx(view.context.resources.displayMetrics))
+        }
+        composeTestRule.activityRule.withActivity {
+            val newBoxWidth = (boxWidth - 10.dp).toPx(resources.displayMetrics)
+            val newBoxHeight = (boxHeight - 10.dp).toPx(resources.displayMetrics)
+            testSandboxedUiAdapter.testSession?.requestResize(newBoxWidth, newBoxHeight) as Any
+        }
+        composeTestRule.waitForIdle()
+        // the request is a no-op when the MeasureSpec is EXACTLY
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+            assertThat(view.width).isEqualTo((boxWidth).toPx(view.context.resources.displayMetrics))
+            assertThat(view.height)
+                .isEqualTo((boxHeight).toPx(view.context.resources.displayMetrics))
+        }
+    }
+
+    @Test
+    fun sandboxedSdkViewIsTransitionGroupTest() {
+        addNodeToLayout()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, exception ->
+            if (!(view as ViewGroup).isTransitionGroup) {
+                throw exception
+            }
+        }
+    }
+
+    @Test
+    fun signalsNotSentWhenViewUnchangedTest() {
+        addNodeToLayout()
+        val session = testSandboxedUiAdapter.testSession
+        session?.runAndRetrieveNextUiChange {}
+        session?.assertNoSubsequentUiChanges()
+    }
+
+    /**
+     * Shifts the view partially off screen and verifies that the reported onScreenGeometry is
+     * cropped accordingly.
+     */
+    @Test
+    fun correctSignalsSentForOnScreenGeometryWhenViewOffScreenTest() {
+        var xOffset by mutableStateOf(0.dp)
+        var yOffset by mutableStateOf(0.dp)
+        val clippedWidth = 300.dp
+        val clippedHeight = 400.dp
+        composeTestRule.setContent {
+            Box(modifier = Modifier.size(width = clippedWidth, height = clippedHeight)) {
+                SandboxedSdkUi(
+                    sandboxedUiAdapter = testSandboxedUiAdapter,
+                    modifier = Modifier.offset(x = xOffset, y = yOffset),
+                    providerUiOnTop = providerUiOnTop,
+                    sandboxedSdkViewEventListener = eventListener
+                )
+            }
+        }
+        var initialHeight = 0
+        var initialWidth = 0
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+            initialHeight = view.height
+            initialWidth = view.width
+        }
+        val session = testSandboxedUiAdapter.testSession
+        val sandboxedSdkViewUiInfo =
+            session?.runAndRetrieveNextUiChange {
+                xOffset = 100.dp
+                yOffset = 200.dp
+                composeTestRule.waitForIdle()
+            }
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+            assertThat(sandboxedSdkViewUiInfo?.uiContainerWidth)
+                .isEqualTo(clippedWidth.toPx(view.context.resources.displayMetrics))
+            assertThat(sandboxedSdkViewUiInfo?.uiContainerHeight)
+                .isEqualTo(clippedHeight.toPx(view.context.resources.displayMetrics))
+            assertThat(sandboxedSdkViewUiInfo?.onScreenGeometry?.height()?.toFloat())
+                .isEqualTo(initialHeight - yOffset.toPx(view.context.resources.displayMetrics))
+            assertThat(sandboxedSdkViewUiInfo?.onScreenGeometry?.width()?.toFloat())
+                .isEqualTo(initialWidth - xOffset.toPx(view.context.resources.displayMetrics))
+        }
+    }
+
+    @Test
+    fun signalsSentWhenPositionChangesTest() {
+        var offset by mutableStateOf(0.dp)
+        composeTestRule.setContent {
+            SandboxedSdkUi(
+                sandboxedUiAdapter = testSandboxedUiAdapter,
+                modifier = Modifier.offset(x = offset),
+                providerUiOnTop = providerUiOnTop,
+                sandboxedSdkViewEventListener = eventListener
+            )
+        }
+        val session = testSandboxedUiAdapter.testSession
+        val sandboxedSdkViewUiInfo =
+            session?.runAndRetrieveNextUiChange {
+                offset = 10.dp
+                composeTestRule.waitForIdle()
+            }
+        val containerWidth = sandboxedSdkViewUiInfo?.uiContainerWidth ?: 0
+        val onScreenWidth = sandboxedSdkViewUiInfo?.onScreenGeometry?.width()?.toFloat()
+        Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, exception ->
+            val expectedWidth =
+                (containerWidth - offset.toPx(view.context.resources.displayMetrics)).toFloat()
+            if (expectedWidth != onScreenWidth) {
+                throw exception
+            }
+        }
+    }
+
+    /**
+     * Creates many UI changes and ensures that these changes are not sent more frequently than
+     * expected.
+     */
+    @Test
+    @SuppressLint("BanThreadSleep") // Deliberate delay for testing
+    fun signalsNotSentMoreFrequentlyThanLimitTest() {
+        addNodeToLayout()
+        val session = testSandboxedUiAdapter.testSession!!
+        for (i in 1..10) {
+            size += (i * 10).dp
+            composeTestRule.waitForIdle()
+            Thread.sleep(100)
+        }
+        assertThat(session.shortestGapBetweenUiChangeEvents)
+            .isAtLeast(SHORTEST_TIME_BETWEEN_SIGNALS_MS)
+    }
+
+    @Test
+    fun signalsSentWhenHostActivityStateChangesTest() {
+        addNodeToLayout()
+        val session = testSandboxedUiAdapter.testSession
+        session?.runAndRetrieveNextUiChange {}
+        // Replace the first activity with a new activity. The onScreenGeometry should now be empty.
+        var sandboxedSdkViewUiInfo =
+            session?.runAndRetrieveNextUiChange {
+                composeTestRule.activityRule.scenario.onActivity {
+                    val intent = Intent(it, SecondActivity::class.java)
+                    it.startActivity(intent)
+                }
+            }
+        assertThat(sandboxedSdkViewUiInfo?.onScreenGeometry?.isEmpty).isTrue()
+        // Return to the first activity. The onScreenGeometry should now be non-empty.
+        sandboxedSdkViewUiInfo = session?.runAndRetrieveNextUiChange { uiDevice.pressBack() }
+        assertThat(sandboxedSdkViewUiInfo?.onScreenGeometry?.isEmpty).isFalse()
+    }
+
+    private fun addNodeToLayout() {
+        composeTestRule.setContent {
+            SandboxedSdkUi(
+                sandboxedUiAdapter = testSandboxedUiAdapter,
+                modifier = Modifier.requiredSize(size),
+                providerUiOnTop = providerUiOnTop,
+                sandboxedSdkViewEventListener = eventListener
+            )
+        }
+    }
+
+    private fun Dp.toPx(displayMetrics: DisplayMetrics) =
+        TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, displayMetrics).roundToInt()
+}
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
index 84015cb..9002a19 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.privacysandbox.ui.client.test
 
 import android.annotation.SuppressLint
@@ -55,19 +54,10 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+// TODO(b/374919355): Create a common test for View and Compose
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class SandboxedSdkViewTest {
-
-    companion object {
-        const val TIMEOUT = 1000.toLong()
-
-        // Longer timeout used for expensive operations like device rotation.
-        const val UI_INTENSIVE_TIMEOUT = 2000.toLong()
-
-        const val SHORTEST_TIME_BETWEEN_SIGNALS_MS = 200
-    }
-
     private lateinit var uiDevice: UiDevice
     private lateinit var context: Context
     private lateinit var view: SandboxedSdkView
@@ -77,23 +67,8 @@
     private lateinit var linearLayout: LinearLayout
     private var mainLayoutWidth = -1
     private var mainLayoutHeight = -1
-
     @get:Rule var activityScenarioRule = ActivityScenarioRule(UiLibActivity::class.java)
 
-    class FailingTestSandboxedUiAdapter : AbstractSandboxedUiAdapter() {
-        override fun openSession(
-            context: Context,
-            windowInputToken: IBinder,
-            initialWidth: Int,
-            initialHeight: Int,
-            isZOrderOnTop: Boolean,
-            clientExecutor: Executor,
-            client: SandboxedUiAdapter.SessionClient
-        ) {
-            clientExecutor.execute { client.onSessionError(Exception("Error in openSession()")) }
-        }
-    }
-
     @Before
     fun setup() {
         context = InstrumentationRegistry.getInstrumentation().targetContext
@@ -125,20 +100,15 @@
     fun addAndRemoveEventListenerTest() {
         // Initially no events are received when the session is not open.
         assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
-
         // When session is open, the events are received
         addViewToLayout()
         assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
         // Remove the view from layout to close the session.
         removeAllViewsFromLayout()
         assertThat(eventListener.sessionClosedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
         eventListener.uiDisplayedLatch = CountDownLatch(1)
-
         // Remove the listener from the view.
         view.setEventListener(null)
-
         // Add view to layout again to start the session. The latches will not count down this time.
         addViewToLayout()
         assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
@@ -150,16 +120,13 @@
         val eventListener2 = TestEventListener()
         view.setEventListener(eventListener1)
         view.setEventListener(eventListener2)
-
         activityScenarioRule.withActivity { view.setAdapter(FailingTestSandboxedUiAdapter()) }
         addViewToLayout()
         assertThat(eventListener1.errorLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
         assertThat(eventListener2.errorLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
         activityScenarioRule.withActivity { view.setAdapter(testSandboxedUiAdapter) }
         assertThat(eventListener1.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
         assertThat(eventListener2.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
         removeAllViewsFromLayout()
         assertThat(eventListener1.sessionClosedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS))
             .isFalse()
@@ -189,11 +156,9 @@
     fun childViewRemovedOnErrorTest() {
         assertTrue(view.childCount == 0)
         addViewToLayout()
-
         testSandboxedUiAdapter.assertSessionOpened()
         assertTrue(view.childCount == 1)
         assertTrue(view.layoutParams == layoutParams)
-
         activityScenarioRule.withActivity {
             testSandboxedUiAdapter.internalClient!!.onSessionError(Exception())
             assertTrue(view.childCount == 0)
@@ -203,19 +168,16 @@
     @Test
     fun onZOrderChangedTest() {
         addViewToLayout()
-
         // When session is opened, the provider should not receive a Z-order notification.
         testSandboxedUiAdapter.assertSessionOpened()
         val session = testSandboxedUiAdapter.testSession!!
         val adapter = testSandboxedUiAdapter
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
         assertThat(adapter.isZOrderOnTop).isTrue()
-
         // When state changes to false, the provider should be notified.
         view.orderProviderUiAboveClientUi(false)
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
         assertThat(adapter.isZOrderOnTop).isFalse()
-
         // When state changes back to true, the provider should be notified.
         session.zOrderChangedLatch = CountDownLatch(1)
         view.orderProviderUiAboveClientUi(true)
@@ -226,13 +188,11 @@
     @Test
     fun onZOrderUnchangedTest() {
         addViewToLayout()
-
         // When session is opened, the provider should not receive a Z-order notification.
         testSandboxedUiAdapter.assertSessionOpened()
         val session = testSandboxedUiAdapter.testSession!!
         val adapter = testSandboxedUiAdapter
         assertThat(adapter.isZOrderOnTop).isTrue()
-
         // When Z-order state is unchanged, the provider should not be notified.
         session.zOrderChangedLatch = CountDownLatch(1)
         view.orderProviderUiAboveClientUi(true)
@@ -246,7 +206,6 @@
         addViewToLayout()
         testSandboxedUiAdapter.assertSessionOpened()
         val session = testSandboxedUiAdapter.testSession!!
-
         // The initial Z-order state is passed to the session, but notifyZOrderChanged is not called
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
         assertThat(testSandboxedUiAdapter.isZOrderOnTop).isFalse()
@@ -261,7 +220,6 @@
         val session = testSandboxedUiAdapter.testSession!!
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
         activityScenarioRule.withActivity { testSandboxedUiAdapter.sendOnSessionOpened() }
-
         // After session has opened, the pending Z order changed made while loading is notified
         // th the session.
         assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
@@ -279,12 +237,13 @@
             Until.newWindow(),
             UI_INTENSIVE_TIMEOUT
         )
-        testSandboxedUiAdapter.assertSessionOpened()
+        testSandboxedUiAdapter.assertSessionNotClosed()
         uiDevice.performActionAndWait(
             { uiDevice.setOrientationNatural() },
             Until.newWindow(),
             UI_INTENSIVE_TIMEOUT
         )
+        testSandboxedUiAdapter.assertSessionNotClosed()
     }
 
     @Test
@@ -301,7 +260,6 @@
     fun overrideProviderViewLayoutParams() {
         val providerViewWidth = (0..1000).random()
         val providerViewHeight = (0..1000).random()
-
         class CustomSession : AbstractSandboxedUiAdapter.AbstractSession() {
             override val view = View(context)
 
@@ -309,7 +267,6 @@
                 view.layoutParams = LinearLayout.LayoutParams(providerViewWidth, providerViewHeight)
             }
         }
-
         class CustomUiAdapter : AbstractSandboxedUiAdapter() {
             override fun openSession(
                 context: Context,
@@ -323,11 +280,9 @@
                 clientExecutor.execute { client.onSessionOpened(CustomSession()) }
             }
         }
-
         view.setAdapter(CustomUiAdapter())
         addViewToLayout(waitToBeActive = true)
         val contentView = view.getChildAt(0)
-
         assertThat(contentView.layoutParams.width).isNotEqualTo(providerViewWidth)
         assertThat(contentView.layoutParams.height).isNotEqualTo(providerViewHeight)
         assertThat(contentView.layoutParams.width).isEqualTo(LinearLayout.LayoutParams.WRAP_CONTENT)
@@ -341,7 +296,6 @@
         val initialWidth = 100
         val initialHeight = 100
         view.layoutParams = LinearLayout.LayoutParams(initialWidth, initialHeight)
-
         class CustomSession : AbstractSandboxedUiAdapter.AbstractSession() {
             override val view = TextView(context)
 
@@ -349,9 +303,7 @@
                 view.text = "Test View"
             }
         }
-
         val customSession = CustomSession()
-
         class CustomUiAdapter : AbstractSandboxedUiAdapter() {
             override fun openSession(
                 context: Context,
@@ -365,12 +317,9 @@
                 clientExecutor.execute { client.onSessionOpened(customSession) }
             }
         }
-
         view.setAdapter(CustomUiAdapter())
         addViewToLayout(waitToBeActive = true)
-
         customSession.view.layout(0, 0, initialWidth * 2, initialHeight * 2)
-
         assertThat(customSession.view.width).isEqualTo(initialWidth * 2)
         assertThat(customSession.view.height).isEqualTo(initialHeight * 2)
         assertThat(view.width).isEqualTo(initialWidth)
@@ -495,11 +444,9 @@
     fun inputTokenIsCorrect() {
         // Input token is only needed when provider can be located on a separate process.
         assumeTrue(BackwardCompatUtil.canProviderBeRemote())
-
         lateinit var layout: LinearLayout
         val surfaceView = SurfaceView(context)
         val surfaceViewLatch = CountDownLatch(1)
-
         var token: IBinder? = null
         surfaceView.addOnAttachStateChangeListener(
             object : View.OnAttachStateChangeListener {
@@ -512,18 +459,15 @@
                 override fun onViewDetachedFromWindow(p0: View) {}
             }
         )
-
         // Attach SurfaceView
         activityScenarioRule.withActivity {
             layout = findViewById(R.id.mainlayout)
             layout.addView(surfaceView)
             layout.removeView(surfaceView)
         }
-
         // Verify SurfaceView has a non-null token when attached.
         assertThat(surfaceViewLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
         assertThat(token).isNotNull()
-
         // Verify that the UI adapter receives the same host token object when opening a session.
         addViewToLayout()
         testSandboxedUiAdapter.assertSessionOpened()
@@ -752,7 +696,6 @@
                 Runnable { view.removeAllViewsInLayout() },
                 Runnable { view.removeViewsInLayout(0, 0) }
             )
-
         removeChildRunnableArray.forEach { removeChildRunnable ->
             val exception =
                 assertThrows(UnsupportedOperationException::class.java) {
@@ -807,4 +750,11 @@
         assertThat(width).isEqualTo(expectedWidth)
         assertThat(height).isEqualTo(expectedHeight)
     }
+
+    companion object {
+        const val TIMEOUT = 1000.toLong()
+        // Longer timeout used for expensive operations like device rotation.
+        const val UI_INTENSIVE_TIMEOUT = 2000.toLong()
+        const val SHORTEST_TIME_BETWEEN_SIGNALS_MS = 200
+    }
 }
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/TestSandboxedUiAdapter.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/TestSandboxedUiAdapter.kt
index 5bfe8df..f9ebe61 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/TestSandboxedUiAdapter.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/TestSandboxedUiAdapter.kt
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.privacysandbox.ui.client.test
 
 import android.content.Context
@@ -32,20 +31,18 @@
 
 class TestSandboxedUiAdapter(private val signalOptions: Set<String> = setOf("option")) :
     AbstractSandboxedUiAdapter() {
-
     var isSessionOpened = false
     var internalClient: SandboxedUiAdapter.SessionClient? = null
     var testSession: TestSession? = null
     var isZOrderOnTop = true
     var inputToken: IBinder? = null
-
     // When set to true, the onSessionOpened callback will only be invoked when specified
     // by the test. This is to test race conditions when the session is being loaded.
     var delayOpenSessionCallback = false
-
     private val openSessionLatch = CountDownLatch(1)
     private val resizeLatch = CountDownLatch(1)
     private val configChangedLatch = CountDownLatch(1)
+    private val sessionClosedLatch = CountDownLatch(1)
 
     override fun openSession(
         context: Context,
@@ -98,16 +95,21 @@
         )
     }
 
+    internal fun assertSessionNotClosed() {
+        Truth.assertThat(
+                sessionClosedLatch.await(SandboxedSdkViewTest.TIMEOUT, TimeUnit.MILLISECONDS)
+            )
+            .isFalse()
+    }
+
     inner class TestSession(context: Context, override val signalOptions: Set<String>) :
         SandboxedUiAdapter.Session {
-
         var zOrderChangedLatch: CountDownLatch = CountDownLatch(1)
         var shortestGapBetweenUiChangeEvents = Long.MAX_VALUE
         private var notifyUiChangedLatch: CountDownLatch = CountDownLatch(1)
         private var latestUiChange: Bundle = Bundle()
         private var hasReceivedFirstUiChange = false
         private var timeReceivedLastUiChange = SystemClock.elapsedRealtime()
-
         override val view: View = View(context)
 
         fun requestResize(width: Int, height: Int) {
@@ -127,7 +129,9 @@
             configChangedLatch.countDown()
         }
 
-        override fun close() {}
+        override fun close() {
+            sessionClosedLatch.countDown()
+        }
 
         override fun notifyUiChanged(uiContainerInfo: Bundle) {
             if (hasReceivedFirstUiChange) {
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt
new file mode 100644
index 0000000..27b1299
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt
@@ -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.
+ */
+package androidx.privacysandbox.ui.client.test
+
+import androidx.activity.ComponentActivity
+
+class UiLibComposeActivity : ComponentActivity() {}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkUi.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkUi.kt
new file mode 100644
index 0000000..29f873a
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkUi.kt
@@ -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.privacysandbox.ui.client.view
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+
+/**
+ * Composable that can be used to remotely render UI from a SandboxedSdk to host app window.
+ *
+ * @param sandboxedUiAdapter an adapter that provides content from a SandboxedSdk to be displayed as
+ *   part of a host app's window.
+ * @param modifier the [Modifier] to be applied to this SandboxedSdkUi.
+ * @param providerUiOnTop sets the Z-ordering of the SandboxedSdkUi surface, relative to its window.
+ * @param sandboxedSdkViewEventListener an event listener to the UI presentation.
+ */
+@Composable
+@Suppress("MissingJvmstatic")
+fun SandboxedSdkUi(
+    sandboxedUiAdapter: SandboxedUiAdapter,
+    modifier: Modifier = Modifier,
+    providerUiOnTop: Boolean = true,
+    sandboxedSdkViewEventListener: SandboxedSdkViewEventListener? = null
+) {
+    val delegatedListener =
+        remember {
+                object : SandboxedSdkViewEventListener {
+                    var delegate by mutableStateOf(sandboxedSdkViewEventListener)
+
+                    override fun onUiDisplayed() {
+                        delegate?.onUiDisplayed()
+                    }
+
+                    override fun onUiError(error: Throwable) {
+                        delegate?.onUiError(error)
+                    }
+
+                    override fun onUiClosed() {
+                        delegate?.onUiClosed()
+                    }
+                }
+            }
+            .apply { delegate = sandboxedSdkViewEventListener }
+    AndroidView(
+        modifier = modifier,
+        factory = { context ->
+            SandboxedSdkView(context).apply { setEventListener(delegatedListener) }
+        },
+        update = { view ->
+            view.setAdapter(sandboxedUiAdapter)
+            view.orderProviderUiAboveClientUi(providerUiOnTop)
+        }
+    )
+}
diff --git a/room/room-paging/build.gradle b/room/room-paging/build.gradle
index e0ab2c1..3805343 100644
--- a/room/room-paging/build.gradle
+++ b/room/room-paging/build.gradle
@@ -41,7 +41,6 @@
                 api(libs.kotlinStdlib)
                 api("androidx.paging:paging-common:3.3.2")
                 api(project(":room:room-runtime"))
-                implementation(libs.atomicFu)
             }
         }
 
@@ -66,6 +65,9 @@
 
         nativeMain {
             dependsOn(jvmNativeMain)
+            dependencies {
+                implementation(libs.atomicFu)
+            }
         }
 
         androidInstrumentedTest {
diff --git a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
index 4128526..66cbfa3 100644
--- a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
+++ b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
@@ -61,7 +61,7 @@
     private val implementation = CommonLimitOffsetImpl(tables, this, ::convertRows)
 
     public actual val itemCount: Int
-        get() = implementation.itemCount.value
+        get() = implementation.itemCount.get()
 
     override val jumpingSupported: Boolean
         get() = true
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index edd5950..9c5783d4 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -24,11 +24,12 @@
 import androidx.room.RoomDatabase
 import androidx.room.RoomRawQuery
 import androidx.room.Transactor.SQLiteTransactionType
+import androidx.room.concurrent.AtomicBoolean
+import androidx.room.concurrent.AtomicInt
 import androidx.room.paging.util.INITIAL_ITEM_COUNT
 import androidx.room.paging.util.queryDatabase
 import androidx.room.paging.util.queryItemCount
 import androidx.room.useReaderConnection
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
@@ -70,9 +71,9 @@
     private val db = pagingSource.db
     private val sourceQuery = pagingSource.sourceQuery
 
-    internal val itemCount = atomic(INITIAL_ITEM_COUNT)
+    internal val itemCount = AtomicInt(INITIAL_ITEM_COUNT)
 
-    private val invalidationFlowStarted = atomic(false)
+    private val invalidationFlowStarted = AtomicBoolean(false)
     private var invalidationFlowJob: Job? = null
 
     init {
@@ -80,7 +81,7 @@
     }
 
     suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {
-        if (invalidationFlowStarted.compareAndSet(expect = false, update = true)) {
+        if (invalidationFlowStarted.compareAndSet(false, true)) {
             invalidationFlowJob =
                 db.getCoroutineScope().launch {
                     db.invalidationTracker.createFlow(*tables, emitInitialState = false).collect {
@@ -92,7 +93,7 @@
                 }
         }
 
-        val tempCount = itemCount.value
+        val tempCount = itemCount.get()
         // if itemCount is < 0, then it is initial load
         return try {
             if (tempCount == INITIAL_ITEM_COUNT) {
@@ -119,7 +120,7 @@
         return db.useReaderConnection { connection ->
             connection.withTransaction(SQLiteTransactionType.DEFERRED) {
                 val tempCount = queryItemCount(sourceQuery, db)
-                itemCount.value = tempCount
+                itemCount.set(tempCount)
                 queryDatabase(
                     params = params,
                     sourceQuery = sourceQuery,
diff --git a/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt b/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt
index 1f4235e..77f4bd2 100644
--- a/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt
+++ b/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt
@@ -41,7 +41,7 @@
     private val implementation = CommonLimitOffsetImpl(tables, this, ::convertRows)
 
     public actual val itemCount: Int
-        get() = implementation.itemCount.value
+        get() = implementation.itemCount.get()
 
     override val jumpingSupported: Boolean
         get() = true
diff --git a/room/room-runtime/bcv/native/current.txt b/room/room-runtime/bcv/native/current.txt
index 984bd87..89d7a96 100644
--- a/room/room-runtime/bcv/native/current.txt
+++ b/room/room-runtime/bcv/native/current.txt
@@ -222,6 +222,24 @@
     final object Companion // androidx.room/EntityUpsertAdapter.Companion|null[0]
 }
 
+final class androidx.room.concurrent/AtomicBoolean { // androidx.room.concurrent/AtomicBoolean|null[0]
+    constructor <init>(kotlin/Boolean) // androidx.room.concurrent/AtomicBoolean.<init>|<init>(kotlin.Boolean){}[0]
+
+    final fun compareAndSet(kotlin/Boolean, kotlin/Boolean): kotlin/Boolean // androidx.room.concurrent/AtomicBoolean.compareAndSet|compareAndSet(kotlin.Boolean;kotlin.Boolean){}[0]
+    final fun get(): kotlin/Boolean // androidx.room.concurrent/AtomicBoolean.get|get(){}[0]
+}
+
+final class androidx.room.concurrent/AtomicInt { // androidx.room.concurrent/AtomicInt|null[0]
+    constructor <init>(kotlin/Int) // androidx.room.concurrent/AtomicInt.<init>|<init>(kotlin.Int){}[0]
+
+    final fun compareAndSet(kotlin/Int, kotlin/Int): kotlin/Boolean // androidx.room.concurrent/AtomicInt.compareAndSet|compareAndSet(kotlin.Int;kotlin.Int){}[0]
+    final fun decrementAndGet(): kotlin/Int // androidx.room.concurrent/AtomicInt.decrementAndGet|decrementAndGet(){}[0]
+    final fun get(): kotlin/Int // androidx.room.concurrent/AtomicInt.get|get(){}[0]
+    final fun getAndIncrement(): kotlin/Int // androidx.room.concurrent/AtomicInt.getAndIncrement|getAndIncrement(){}[0]
+    final fun incrementAndGet(): kotlin/Int // androidx.room.concurrent/AtomicInt.incrementAndGet|incrementAndGet(){}[0]
+    final fun set(kotlin/Int) // androidx.room.concurrent/AtomicInt.set|set(kotlin.Int){}[0]
+}
+
 final class androidx.room.util/ByteArrayWrapper { // androidx.room.util/ByteArrayWrapper|null[0]
     constructor <init>(kotlin/ByteArray) // androidx.room.util/ByteArrayWrapper.<init>|<init>(kotlin.ByteArray){}[0]
 
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index dc736fd..4fd3768 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -110,7 +110,6 @@
                 api("androidx.collection:collection:1.4.2")
                 api("androidx.annotation:annotation:1.8.1")
                 api(libs.kotlinCoroutinesCore)
-                implementation(libs.atomicFu)
             }
         }
         commonTest {
@@ -180,6 +179,7 @@
             dependsOn(jvmNativeMain)
             dependencies {
                 api(project(":sqlite:sqlite-framework"))
+                implementation(libs.atomicFu)
             }
         }
         nativeTest {
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
index ab9a93b..f876d37 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.kruth.assertThat
 import androidx.room.Transactor
+import androidx.room.concurrent.AtomicInt
 import androidx.sqlite.SQLiteDriver
 import androidx.sqlite.driver.bundled.BundledSQLiteDriver
 import androidx.test.filters.LargeTest
@@ -27,7 +28,6 @@
 import kotlin.test.AfterTest
 import kotlin.test.BeforeTest
 import kotlin.test.Test
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.InternalCoroutinesApi
@@ -130,7 +130,7 @@
 
     /** A CoroutineDispatcher that dispatches every block into a new thread */
     private class NewThreadDispatcher : CoroutineDispatcher() {
-        private val idCounter = atomic(0)
+        private val idCounter = AtomicInt(0)
 
         @OptIn(InternalCoroutinesApi::class)
         override fun dispatchYield(context: CoroutineContext, block: Runnable) {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
index 04caeb7..9b5e5d8 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
@@ -21,12 +21,12 @@
 import androidx.annotation.WorkerThread
 import androidx.lifecycle.LiveData
 import androidx.room.InvalidationTracker.Observer
+import androidx.room.concurrent.ReentrantLock
+import androidx.room.concurrent.withLock
 import androidx.room.support.AutoCloser
 import androidx.sqlite.SQLiteConnection
 import java.lang.ref.WeakReference
 import java.util.concurrent.Callable
-import kotlinx.atomicfu.locks.reentrantLock
-import kotlinx.atomicfu.locks.withLock
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.runBlocking
@@ -65,7 +65,7 @@
         )
 
     private val observerMap = mutableMapOf<Observer, ObserverWrapper>()
-    private val observerMapLock = reentrantLock()
+    private val observerMapLock = ReentrantLock()
 
     private var autoCloser: AutoCloser? = null
 
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
index bbe94dc..c5e76f9 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
@@ -19,6 +19,8 @@
 import androidx.annotation.RequiresApi
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
+import androidx.room.concurrent.AtomicBoolean
+import androidx.room.concurrent.AtomicInt
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
 import androidx.sqlite.SQLiteStatement
@@ -27,7 +29,6 @@
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlin.collections.removeFirst as removeFirstKt
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.cancelAndJoin
@@ -346,12 +347,12 @@
     fun refreshAndCloseDbWithSlowObserver() = runTest {
         // Validates that a slow observer will finish notification after database closing
         val invalidatedLatch = CountDownLatch(1)
-        val invalidated = atomic(false)
+        val invalidated = AtomicBoolean(false)
         tracker.addObserver(
             object : InvalidationTracker.Observer("a") {
                 override fun onInvalidated(tables: Set<String>) {
                     invalidatedLatch.countDown()
-                    assertThat(invalidated.compareAndSet(expect = false, update = true)).isTrue()
+                    assertThat(invalidated.compareAndSet(false, true)).isTrue()
                     runBlocking { delay(100) }
                 }
             }
@@ -361,7 +362,7 @@
         testScheduler.advanceUntilIdle()
         invalidatedLatch.await()
         roomDatabase.close()
-        assertThat(invalidated.value).isTrue()
+        assertThat(invalidated.get()).isTrue()
     }
 
     @Test
@@ -473,7 +474,7 @@
 
     @Test
     fun weakObserver() = runTest {
-        val invalidated = atomic(0)
+        val invalidated = AtomicInt(0)
         var observer: InvalidationTracker.Observer? =
             object : InvalidationTracker.Observer("a") {
                 override fun onInvalidated(tables: Set<String>) {
@@ -484,7 +485,7 @@
 
         sqliteDriver.setInvalidatedTables(0)
         tracker.awaitRefreshAsync()
-        assertThat(invalidated.value).isEqualTo(1)
+        assertThat(invalidated.get()).isEqualTo(1)
 
         // Attempt to perform garbage collection in a loop so that weak observer is discarded
         // and it stops receiving invalidation notifications. If GC fails to collect the observer
@@ -511,7 +512,7 @@
 
         sqliteDriver.setInvalidatedTables(0)
         tracker.awaitRefreshAsync()
-        assertThat(invalidated.value).isEqualTo(1)
+        assertThat(invalidated.get()).isEqualTo(1)
     }
 
     @Test
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
index 1997321..5730b60 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
@@ -18,16 +18,16 @@
 
 import androidx.annotation.RestrictTo
 import androidx.room.Transactor.SQLiteTransactionType
+import androidx.room.concurrent.AtomicBoolean
+import androidx.room.concurrent.ReentrantLock
 import androidx.room.concurrent.ifNotClosed
+import androidx.room.concurrent.withLock
 import androidx.room.util.getCoroutineContext
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteException
 import androidx.sqlite.execSQL
 import kotlin.jvm.JvmOverloads
 import kotlin.jvm.JvmSuppressWildcards
-import kotlinx.atomicfu.atomic
-import kotlinx.atomicfu.locks.reentrantLock
-import kotlinx.atomicfu.locks.withLock
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.FlowCollector
@@ -162,7 +162,7 @@
      * queue to be done asynchronously, this flag is used to control excessive scheduling of
      * refreshes.
      */
-    private val pendingRefresh = atomic(false)
+    private val pendingRefresh = AtomicBoolean(false)
 
     /** Callback to allow or disallow [refreshInvalidation] from proceeding. */
     internal var onAllowRefresh: () -> Boolean = { true }
@@ -484,7 +484,7 @@
  */
 internal class ObservedTableStates(size: Int) {
 
-    private val lock = reentrantLock()
+    private val lock = ReentrantLock()
 
     // The number of observers per table
     private val tableObserversCount = LongArray(size)
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Atomics.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Atomics.kt
new file mode 100644
index 0000000..04abb13
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Atomics.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+
+package androidx.room.concurrent
+
+import androidx.annotation.RestrictTo
+
+expect class AtomicInt {
+    constructor(initialValue: Int)
+
+    fun get(): Int
+
+    fun set(value: Int)
+
+    fun compareAndSet(expect: Int, update: Int): Boolean
+
+    fun incrementAndGet(): Int
+
+    fun getAndIncrement(): Int
+
+    fun decrementAndGet(): Int
+}
+
+internal inline fun AtomicInt.loop(action: (Int) -> Unit): Nothing {
+    while (true) {
+        action(get())
+    }
+}
+
+expect class AtomicBoolean {
+    constructor(initialValue: Boolean)
+
+    fun get(): Boolean
+
+    fun compareAndSet(expect: Boolean, update: Boolean): Boolean
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt
index 8bf4a24..e499315 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt
@@ -16,11 +16,6 @@
 
 package androidx.room.concurrent
 
-import kotlinx.atomicfu.atomic
-import kotlinx.atomicfu.locks.SynchronizedObject
-import kotlinx.atomicfu.locks.synchronized
-import kotlinx.atomicfu.loop
-
 /**
  * A barrier that can be used to perform a cleanup action once, waiting for registered parties
  * (blockers) to finish using the protected resource.
@@ -40,9 +35,10 @@
  *   blockers.
  */
 internal class CloseBarrier(private val closeAction: () -> Unit) : SynchronizedObject() {
-    private val blockers = atomic(0)
-    private val closeInitiated = atomic(false)
-    private val isClosed by closeInitiated
+    private val blockers = AtomicInt(0)
+    private val closeInitiated = AtomicBoolean(false)
+    private val isClosed: Boolean
+        get() = closeInitiated.get()
 
     /**
      * Blocks the [closeAction] from occurring.
@@ -72,7 +68,7 @@
     internal fun unblock(): Unit =
         synchronized(this) {
             blockers.decrementAndGet()
-            check(blockers.value >= 0) { "Unbalanced call to unblock() detected." }
+            check(blockers.get() >= 0) { "Unbalanced call to unblock() detected." }
         }
 
     /**
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt
index de1c2e5..2d34dd7 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt
@@ -16,11 +16,6 @@
 
 package androidx.room.concurrent
 
-import kotlinx.atomicfu.locks.ReentrantLock
-import kotlinx.atomicfu.locks.SynchronizedObject
-import kotlinx.atomicfu.locks.reentrantLock
-import kotlinx.atomicfu.locks.synchronized
-
 /**
  * An exclusive lock for in-process and multi-process synchronization.
  *
@@ -59,7 +54,7 @@
 
         private fun getThreadLock(key: String): ReentrantLock =
             synchronized(this) {
-                return threadLocksMap.getOrPut(key) { reentrantLock() }
+                return threadLocksMap.getOrPut(key) { ReentrantLock() }
             }
 
         private fun getFileLock(key: String) = FileLock(key)
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ReentrantLock.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ReentrantLock.kt
new file mode 100644
index 0000000..444ef7c
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ReentrantLock.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.concurrent
+
+internal expect class ReentrantLock() {
+    fun lock(): Unit
+
+    fun tryLock(): Boolean
+
+    fun unlock(): Unit
+}
+
+internal inline fun <T> ReentrantLock.withLock(block: () -> T): T {
+    lock()
+    try {
+        return block()
+    } finally {
+        unlock()
+    }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Synchronized.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Synchronized.kt
new file mode 100644
index 0000000..1515bdf
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Synchronized.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.concurrent
+
+internal expect open class SynchronizedObject()
+
+internal expect inline fun <T> synchronized(lock: SynchronizedObject, block: () -> T): T
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
index 185e412..ae49a2e 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
@@ -19,6 +19,8 @@
 import androidx.room.TransactionScope
 import androidx.room.Transactor
 import androidx.room.Transactor.SQLiteTransactionType
+import androidx.room.concurrent.AtomicBoolean
+import androidx.room.concurrent.AtomicInt
 import androidx.room.concurrent.ThreadLocal
 import androidx.room.concurrent.asContextElement
 import androidx.room.concurrent.currentThreadId
@@ -35,7 +37,6 @@
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.coroutineContext
 import kotlin.time.Duration.Companion.seconds
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.sync.Mutex
@@ -50,8 +51,9 @@
 
     private val threadLocal = ThreadLocal<PooledConnectionImpl>()
 
-    private val _isClosed = atomic(false)
-    private val isClosed by _isClosed
+    private val _isClosed = AtomicBoolean(false)
+    private val isClosed: Boolean
+        get() = _isClosed.get()
 
     // Amount of time to wait to acquire a connection before throwing, Android uses 30 seconds in
     // its pool, so we do too here, but IDK if that is a good number. This timeout is unrelated to
@@ -195,7 +197,7 @@
 }
 
 private class Pool(val capacity: Int, val connectionFactory: () -> SQLiteConnection) {
-    private val size = atomic(0)
+    private val size = AtomicInt(0)
     private val connections = arrayOfNulls<ConnectionWithLock>(capacity)
     private val channel =
         Channel<ConnectionWithLock>(capacity = capacity, onUndeliveredElement = { recycle(it) })
@@ -211,7 +213,7 @@
     }
 
     private fun tryOpenNewConnection() {
-        val currentSize = size.value
+        val currentSize = size.get()
         if (currentSize >= capacity) {
             // Capacity reached
             return
@@ -322,8 +324,9 @@
 ) : Transactor, RawConnectionAccessor {
     private val transactionStack = ArrayDeque<TransactionItem>()
 
-    private val _isRecycled = atomic(false)
-    private val isRecycled by _isRecycled
+    private val _isRecycled = AtomicBoolean(false)
+    private val isRecycled: Boolean
+        get() = _isRecycled.get()
 
     override val rawConnection: SQLiteConnection
         get() = delegate
diff --git a/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt b/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt
index 75f64d1..3133b3a 100644
--- a/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt
+++ b/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt
@@ -19,7 +19,6 @@
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
 import kotlin.test.Test
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
@@ -34,9 +33,9 @@
     @Test
     @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
     fun oneBlocker() = runTest {
-        val actionPerformed = atomic(false)
+        val actionPerformed = AtomicBoolean(false)
         val closeBarrier = CloseBarrier {
-            assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+            assertThat(actionPerformed.compareAndSet(false, true)).isTrue()
         }
         val jobLaunched = Mutex(locked = true)
 
@@ -52,14 +51,14 @@
 
         // yield for launch and verify the close action has not been performed
         yield()
-        jobLaunched.withLock { assertThat(actionPerformed.value).isFalse() }
+        jobLaunched.withLock { assertThat(actionPerformed.get()).isFalse() }
 
         // unblock the barrier, close job should complete
         closeBarrier.unblock()
         closeJob.join()
 
         // verify action was performed
-        assertThat(actionPerformed.value).isTrue()
+        assertThat(actionPerformed.get()).isTrue()
 
         // verify a new block is not granted since the barrier is already close
         assertThat(closeBarrier.block()).isFalse()
@@ -67,15 +66,15 @@
 
     @Test
     fun noBlockers() = runTest {
-        val actionPerformed = atomic(false)
+        val actionPerformed = AtomicBoolean(false)
         val closeBarrier = CloseBarrier {
-            assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+            assertThat(actionPerformed.compareAndSet(false, true)).isTrue()
         }
 
         // Validate close action is performed immediately if there are no blockers
         closeBarrier.close()
 
-        assertThat(actionPerformed.value).isTrue()
+        assertThat(actionPerformed.get()).isTrue()
     }
 
     @Test
@@ -89,9 +88,9 @@
     @Test
     @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
     fun noStarvation() = runTest {
-        val actionPerformed = atomic(false)
+        val actionPerformed = AtomicBoolean(false)
         val closeBarrier = CloseBarrier {
-            assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+            assertThat(actionPerformed.compareAndSet(false, true)).isTrue()
         }
         val jobLaunched = Mutex(locked = true)
 
@@ -111,12 +110,12 @@
         // yield for launch and verify the close action has not been performed in an attempt to
         // get the block / unblock loop going
         yield()
-        jobLaunched.withLock { assertThat(actionPerformed.value).isFalse() }
+        jobLaunched.withLock { assertThat(actionPerformed.get()).isFalse() }
 
         // initiate the close action, test should not deadlock (or timeout) meaning the barrier
         // will not cause the caller to starve
         closeBarrier.close()
         blockerJob.join()
-        assertThat(actionPerformed.value).isTrue()
+        assertThat(actionPerformed.get()).isTrue()
     }
 }
diff --git a/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt b/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
index cff6dbd..f86b7b6 100644
--- a/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
+++ b/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
@@ -19,6 +19,7 @@
 import androidx.kruth.assertThat
 import androidx.room.PooledConnection
 import androidx.room.Transactor
+import androidx.room.concurrent.AtomicInt
 import androidx.room.deferredTransaction
 import androidx.room.exclusiveTransaction
 import androidx.room.execSQL
@@ -34,7 +35,6 @@
 import kotlin.test.Test
 import kotlin.test.assertFailsWith
 import kotlin.test.fail
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -463,7 +463,7 @@
     @Test
     fun singleConnectionPool() = runTest {
         val multiThreadContext = newFixedThreadPoolContext(2, "Test-Threads")
-        val connectionsOpened = atomic(0)
+        val connectionsOpened = AtomicInt(0)
         val actualDriver = setupDriver()
         val driver =
             object : SQLiteDriver by actualDriver {
@@ -487,12 +487,12 @@
         jobs.joinAll()
         pool.close()
         multiThreadContext.close()
-        assertThat(connectionsOpened.value).isEqualTo(1)
+        assertThat(connectionsOpened.get()).isEqualTo(1)
     }
 
     @Test
     fun openOneConnectionWhenUsedSerially() = runTest {
-        val connectionsOpened = atomic(0)
+        val connectionsOpened = AtomicInt(0)
         val actualDriver = setupDriver()
         val driver =
             object : SQLiteDriver by actualDriver {
@@ -518,7 +518,7 @@
             }
         }
         pool.close()
-        assertThat(connectionsOpened.value).isEqualTo(1)
+        assertThat(connectionsOpened.get()).isEqualTo(1)
     }
 
     @Test
@@ -689,7 +689,7 @@
                 actual.close()
             }
         }
-        val connectionArrCount = atomic(0)
+        val connectionArrCount = AtomicInt(0)
         val connectionsArr = arrayOfNulls<CloseAwareConnection>(4)
         val actualDriver = setupDriver()
         val driver =
@@ -714,7 +714,7 @@
                 launch(multiThreadContext) { pool.useReaderConnection { barrier.withLock {} } }
             jobs.add(job)
         }
-        while (connectionArrCount.value < 4) {
+        while (connectionArrCount.get() < 4) {
             delay(100)
         }
         barrier.unlock()
diff --git a/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Atomics.jvmAndroid.kt b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Atomics.jvmAndroid.kt
new file mode 100644
index 0000000..00255dc
--- /dev/null
+++ b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Atomics.jvmAndroid.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+
+package androidx.room.concurrent
+
+import androidx.annotation.RestrictTo
+
+actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger
+
+actual typealias AtomicBoolean = java.util.concurrent.atomic.AtomicBoolean
diff --git a/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/ReentrantLock.jvmAndroid.kt b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/ReentrantLock.jvmAndroid.kt
new file mode 100644
index 0000000..dc8fdab
--- /dev/null
+++ b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/ReentrantLock.jvmAndroid.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.concurrent
+
+internal actual typealias ReentrantLock = java.util.concurrent.locks.ReentrantLock
diff --git a/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Synchronized.jvmAndroid.kt b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Synchronized.jvmAndroid.kt
new file mode 100644
index 0000000..fae30d5
--- /dev/null
+++ b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Synchronized.jvmAndroid.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.concurrent
+
+internal actual typealias SynchronizedObject = Any
+
+internal actual inline fun <T> synchronized(lock: SynchronizedObject, block: () -> T): T =
+    kotlin.synchronized(lock, block)
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Atomics.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Atomics.native.kt
new file mode 100644
index 0000000..46b4012
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Atomics.native.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+
+package androidx.room.concurrent
+
+import androidx.annotation.RestrictTo
+import kotlin.concurrent.AtomicInt as KotlinAtomicInt
+
+actual class AtomicInt actual constructor(initialValue: Int) {
+    private val delegate: KotlinAtomicInt = KotlinAtomicInt(initialValue)
+
+    actual fun get(): Int = delegate.value
+
+    actual fun set(value: Int) {
+        delegate.value = value
+    }
+
+    actual fun compareAndSet(expect: Int, update: Int): Boolean =
+        delegate.compareAndSet(expect, update)
+
+    actual fun incrementAndGet(): Int = delegate.incrementAndGet()
+
+    actual fun getAndIncrement(): Int = delegate.getAndIncrement()
+
+    actual fun decrementAndGet(): Int = delegate.decrementAndGet()
+}
+
+actual class AtomicBoolean actual constructor(initialValue: Boolean) {
+    private val delegate: KotlinAtomicInt = KotlinAtomicInt(toInt(initialValue))
+
+    actual fun get(): Boolean = delegate.value == 1
+
+    actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean =
+        delegate.compareAndSet(toInt(expect), toInt(update))
+
+    private fun toInt(value: Boolean) = if (value) 1 else 0
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/ReentrantLock.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/ReentrantLock.native.kt
new file mode 100644
index 0000000..544fa01
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/ReentrantLock.native.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.concurrent
+
+internal actual typealias ReentrantLock = kotlinx.atomicfu.locks.SynchronizedObject
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Synchronized.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Synchronized.native.kt
new file mode 100644
index 0000000..7691045
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Synchronized.native.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.concurrent
+
+internal actual typealias SynchronizedObject = kotlinx.atomicfu.locks.SynchronizedObject
+
+internal actual inline fun <T> synchronized(lock: SynchronizedObject, block: () -> T): T =
+    kotlinx.atomicfu.locks.synchronized(lock, block)
diff --git a/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt b/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
index 8fd4b86..616baa6 100644
--- a/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
+++ b/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
@@ -18,6 +18,8 @@
 
 package androidx.savedstate
 
+import android.os.Parcel
+import android.os.Parcelable
 import androidx.annotation.Sampled
 import androidx.savedstate.serialization.decodeFromSavedState
 import androidx.savedstate.serialization.encodeToSavedState
@@ -96,7 +98,6 @@
     val uuid = decodeFromSavedState(UUIDSerializer(), uuidSavedState)
 }
 
-@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") // The lint warning does not show up for external users.
 @Sampled
 fun savedStateSerializer() {
     @Serializable
@@ -125,20 +126,36 @@
     )
 }
 
+private class MyJavaSerializable : java.io.Serializable
+
+private class MyJavaSerializableSerializer : JavaSerializableSerializer<MyJavaSerializable>()
+
 @Sampled
 fun serializableSerializer() {
     @Serializable
     data class MyModel(
-        @Serializable(with = JavaSerializableSerializer::class)
-        val serializable: java.io.Serializable
+        @Serializable(with = MyJavaSerializableSerializer::class)
+        val serializable: MyJavaSerializable
     )
 }
 
+private class MyParcelable : Parcelable {
+    override fun describeContents(): Int {
+        TODO("Not yet implemented")
+    }
+
+    override fun writeToParcel(dest: Parcel, flags: Int) {
+        TODO("Not yet implemented")
+    }
+}
+
+private class MyParcelableSerializer : ParcelableSerializer<MyParcelable>()
+
 @Sampled
 fun parcelableSerializer() {
     @Serializable
     data class MyModel(
-        @Serializable(with = ParcelableSerializer::class) val parcelable: android.os.Parcelable
+        @Serializable(with = MyParcelableSerializer::class) val parcelable: MyParcelable
     )
 }
 
@@ -172,13 +189,11 @@
 fun charSequenceListSerializer() {
     @Serializable
     class MyModel(
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
         @Serializable(with = CharSequenceListSerializer::class)
         val charSequenceList: List<CharSequence>
     )
 }
 
-@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
 @Sampled
 fun parcelableListSerializer() {
     @Serializable
diff --git a/savedstate/savedstate/api/current.txt b/savedstate/savedstate/api/current.txt
index a3e7083..8a63bfe 100644
--- a/savedstate/savedstate/api/current.txt
+++ b/savedstate/savedstate/api/current.txt
@@ -195,12 +195,12 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class CharSequenceSerializer<T extends java.lang.CharSequence> implements kotlinx.serialization.KSerializer<T> {
+  public final class CharSequenceSerializer implements kotlinx.serialization.KSerializer<java.lang.CharSequence> {
     ctor public CharSequenceSerializer();
-    method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
-    method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
-    method public final void serialize(kotlinx.serialization.encoding.Encoder encoder, T value);
-    property public final kotlinx.serialization.descriptors.SerialDescriptor descriptor;
+    method public CharSequence deserialize(kotlinx.serialization.encoding.Decoder decoder);
+    method public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
+    method public void serialize(kotlinx.serialization.encoding.Encoder encoder, CharSequence value);
+    property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
   public final class IBinderSerializer implements kotlinx.serialization.KSerializer<android.os.IBinder> {
@@ -211,7 +211,7 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
+  public abstract class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
     ctor public JavaSerializableSerializer();
     method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
     method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
@@ -235,7 +235,7 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
+  public abstract class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
     ctor public ParcelableSerializer();
     method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
     method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
diff --git a/savedstate/savedstate/api/restricted_current.txt b/savedstate/savedstate/api/restricted_current.txt
index eb7a636..0f65840 100644
--- a/savedstate/savedstate/api/restricted_current.txt
+++ b/savedstate/savedstate/api/restricted_current.txt
@@ -220,12 +220,12 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class CharSequenceSerializer<T extends java.lang.CharSequence> implements kotlinx.serialization.KSerializer<T> {
+  public final class CharSequenceSerializer implements kotlinx.serialization.KSerializer<java.lang.CharSequence> {
     ctor public CharSequenceSerializer();
-    method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
-    method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
-    method public final void serialize(kotlinx.serialization.encoding.Encoder encoder, T value);
-    property public final kotlinx.serialization.descriptors.SerialDescriptor descriptor;
+    method public CharSequence deserialize(kotlinx.serialization.encoding.Decoder decoder);
+    method public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
+    method public void serialize(kotlinx.serialization.encoding.Encoder encoder, CharSequence value);
+    property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
   public final class IBinderSerializer implements kotlinx.serialization.KSerializer<android.os.IBinder> {
@@ -236,7 +236,7 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
+  public abstract class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
     ctor public JavaSerializableSerializer();
     method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
     method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
@@ -260,7 +260,7 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
+  public abstract class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
     ctor public ParcelableSerializer();
     method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
     method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
diff --git a/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
new file mode 100644
index 0000000..edf79cc
--- /dev/null
+++ b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.savedstate
+
+import android.os.Parcel
+
+actual fun platformEncodeDecode(savedState: SavedState): SavedState {
+    val parcel =
+        Parcel.obtain().apply {
+            savedState.writeToParcel(this, 0)
+            setDataPosition(0)
+        }
+    return SavedState.CREATOR.createFromParcel(parcel)
+}
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
index f0dacc5..071f4c1 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
@@ -107,10 +107,10 @@
  * @see androidx.savedstate.serialization.decodeFromSavedState
  */
 @OptIn(ExperimentalSerializationApi::class)
-public open class CharSequenceSerializer<T : CharSequence> : KSerializer<T> {
-    final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CharSequence")
+public class CharSequenceSerializer : KSerializer<CharSequence> {
+    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CharSequence")
 
-    final override fun serialize(encoder: Encoder, value: T) {
+    override fun serialize(encoder: Encoder, value: CharSequence) {
         require(encoder is SavedStateEncoder) {
             encoderErrorMessage(descriptor.serialName, encoder)
         }
@@ -118,17 +118,18 @@
     }
 
     @Suppress("UNCHECKED_CAST")
-    final override fun deserialize(decoder: Decoder): T {
+    override fun deserialize(decoder: Decoder): CharSequence {
         require(decoder is SavedStateDecoder) {
             decoderErrorMessage(descriptor.serialName, decoder)
         }
-        return decoder.run { savedState.read { getCharSequence(key) as T } }
+        return decoder.run { savedState.read { getCharSequence(key) } }
     }
 }
 
 /**
  * A serializer for [java.io.Serializable]. This serializer uses [SavedState]'s API directly to
- * save/load a [java.io.Serializable].
+ * save/load a [java.io.Serializable]. You must extend this serializer for each of your
+ * [java.io.Serializable] subclasses.
  *
  * Note that this serializer should be used with [SavedStateEncoder] or [SavedStateDecoder] only.
  * Using it with other Encoders/Decoders may throw [IllegalArgumentException].
@@ -138,7 +139,7 @@
  * @see androidx.savedstate.serialization.decodeFromSavedState
  */
 @OptIn(ExperimentalSerializationApi::class)
-public open class JavaSerializableSerializer<T : JavaSerializable> : KSerializer<T> {
+public abstract class JavaSerializableSerializer<T : JavaSerializable> : KSerializer<T> {
     final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("JavaSerializable")
 
     final override fun serialize(encoder: Encoder, value: T) {
@@ -159,7 +160,7 @@
 
 /**
  * A serializer for [Parcelable]. This serializer uses [SavedState]'s API directly to save/load a
- * [Parcelable].
+ * [Parcelable]. You must extend this serializer for each of your [Parcelable] subclasses.
  *
  * Note that this serializer should be used with [SavedStateEncoder] or [SavedStateDecoder] only.
  * Using it with other Encoders/Decoders may throw [IllegalArgumentException].
@@ -169,7 +170,7 @@
  * @see androidx.savedstate.serialization.decodeFromSavedState
  */
 @OptIn(ExperimentalSerializationApi::class)
-public open class ParcelableSerializer<T : Parcelable> : KSerializer<T> {
+public abstract class ParcelableSerializer<T : Parcelable> : KSerializer<T> {
     final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Parcelable")
 
     final override fun serialize(encoder: Encoder, value: T) {
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
index 2c34ddb..743c3e4 100644
--- a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
@@ -26,7 +26,6 @@
 import android.util.SizeF
 import android.util.SparseArray
 import androidx.core.os.bundleOf
-import androidx.core.util.forEach
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
 import androidx.savedstate.SavedStateCodecTestUtils.encodeDecode
@@ -59,7 +58,6 @@
 import kotlinx.serialization.encoding.Encoder
 import kotlinx.serialization.encoding.decodeStructure
 import kotlinx.serialization.encoding.encodeStructure
-import kotlinx.serialization.serializer
 
 @ExperimentalSerializationApi
 internal class SavedStateCodecAndroidTest : RobolectricTest() {
@@ -100,18 +98,7 @@
             "SERIALIZER_TYPE_INCOMPATIBLE"
         ) // The lint warning does not show up for external users.
         @Serializable
-        class MyClass(@Serializable(with = SavedStateSerializer::class) val s: Bundle) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as MyClass
-                return s.read { contentDeepEquals(other.s) }
-            }
-
-            override fun hashCode(): Int {
-                return s.read { contentDeepHashCode() }
-            }
-        }
+        class MyClass(@Serializable(with = SavedStateSerializer::class) val s: Bundle)
         MyClass(
                 bundleOf(
                     "i" to 1,
@@ -120,19 +107,24 @@
                     "ss" to bundleOf("s" to "bar")
                 )
             )
-            .encodeDecode {
-                assertThat(size()).isEqualTo(1)
-                getSavedState("s").read {
-                    assertThat(size()).isEqualTo(4)
-                    assertThat(getInt("i")).isEqualTo(1)
-                    assertThat(getString("s")).isEqualTo("foo")
-                    assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
-                    getSavedState("ss").read {
-                        assertThat(size()).isEqualTo(1)
-                        assertThat(getString("s")).isEqualTo("bar")
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    assertThat(decoded.s.read { contentDeepEquals(original.s) }).isTrue()
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    getSavedState("s").read {
+                        assertThat(size()).isEqualTo(4)
+                        assertThat(getInt("i")).isEqualTo(1)
+                        assertThat(getString("s")).isEqualTo("foo")
+                        assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
+                        getSavedState("ss").read {
+                            assertThat(size()).isEqualTo(1)
+                            assertThat(getString("s")).isEqualTo("bar")
+                        }
                     }
                 }
-            }
+            )
 
         // Bundle at root.
         val origin = bundleOf("i" to 3, "s" to "foo", "d" to 3.14)
@@ -180,7 +172,8 @@
 
         @Serializable
         data class SerializableContainer(
-            @Serializable(with = JavaSerializableSerializer::class) val value: java.io.Serializable
+            @Serializable(with = CustomJavaSerializableSerializer::class)
+            val value: java.io.Serializable
         )
         val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
         SerializableContainer(myJavaSerializable).encodeDecode {
@@ -191,7 +184,7 @@
 
         @Serializable
         data class ParcelableContainer(
-            @Serializable(with = ParcelableSerializer::class) val value: Parcelable
+            @Serializable(with = CustomParcelableSerializer::class) val value: Parcelable
         )
         val myParcelable = MyParcelable(3, "foo", 3.14)
         ParcelableContainer(myParcelable).encodeDecode {
@@ -257,19 +250,9 @@
             error("VERSION.SDK_INT < Q")
         }
 
+        @Suppress("ArrayInDataClass")
         @Serializable
-        data class CharSequenceArrayContainer(val value: Array<out CharSequence>) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as CharSequenceArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
-            }
-        }
+        data class CharSequenceArrayContainer(val value: Array<out CharSequence>)
         assertThrows<SerializationException> {
                 CharSequenceArrayContainer(arrayOf("foo", "bar")).encodeDecode {}
             }
@@ -281,20 +264,10 @@
 
     @Test
     fun concreteTypesInsteadOfInterfaceTypes() {
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
-        @Serializable
-        data class CharSequenceContainer(
-            @Serializable(with = CharSequenceSerializer::class) val value: String
-        )
-        CharSequenceContainer("foo").encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequence("value")).isEqualTo("foo")
-        }
-
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
         @Serializable
         data class SerializableContainer(
-            @Serializable(with = JavaSerializableSerializer::class) val value: MyJavaSerializable
+            @Serializable(with = MyJavaSerializableAsJavaSerializableSerializer::class)
+            val value: MyJavaSerializable
         )
         val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
         SerializableContainer(myJavaSerializable).encodeDecode {
@@ -303,10 +276,9 @@
                 .isEqualTo(myJavaSerializable)
         }
 
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
         @Serializable
         data class ParcelableContainer(
-            @Serializable(with = ParcelableSerializer::class) val value: MyParcelable
+            @Serializable(with = MyParcelableAsParcelableSerializer::class) val value: MyParcelable
         )
         val myParcelable = MyParcelable(3, "foo", 3.14)
         ParcelableContainer(myParcelable).encodeDecode {
@@ -315,10 +287,11 @@
         }
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
             @Serializable
             data class IBinderContainer(
-                @Serializable(with = IBinderSerializer::class) val value: Binder
+                @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+                @Serializable(with = IBinderSerializer::class)
+                val value: Binder
             )
             val binder = Binder("foo")
             IBinderContainer(binder).encodeDecode {
@@ -333,47 +306,36 @@
     @Test
     fun collectionTypes() {
         @Serializable
+        @Suppress("ArrayInDataClass")
         data class CharSequenceArrayContainer(
             @Serializable(with = CharSequenceArraySerializer::class)
             val value: Array<out CharSequence>
-        ) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as CharSequenceArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
-            }
-        }
-        val myCharSequenceArray = arrayOf("foo", "bar")
-        CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
-        }
+        )
+        val myCharSequenceArray = arrayOf(StringBuilder("foo"), StringBuilder("bar"))
+        CharSequenceArrayContainer(myCharSequenceArray)
+            .encodeDecode(
+                checkDecoded = { decoded, original -> decoded.value.contentEquals(original.value) },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
+                }
+            )
 
         @Serializable
+        @Suppress("ArrayInDataClass")
         data class ParcelableArrayContainer(
             @Serializable(with = ParcelableArraySerializer::class) val value: Array<out Parcelable>
-        ) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as ParcelableArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
-            }
-        }
+        )
         val myParcelableArray = arrayOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
-        ParcelableArrayContainer(myParcelableArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
-        }
+        ParcelableArrayContainer(myParcelableArray)
+            .encodeDecode(
+                checkDecoded = { decoded, original -> decoded.value.contentEquals(original.value) },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getParcelableArray<MyParcelable>("value"))
+                        .isEqualTo(myParcelableArray)
+                }
+            )
 
         @Serializable
         data class CharSequenceListContainer(
@@ -406,76 +368,96 @@
                 append(1, MyParcelable(3, "foo", 3.14))
                 append(3, MyParcelable(4, "bar", 1.73))
             }
-        SparseParcelableArrayContainer(mySparseParcelableArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getSparseParcelableArray<Parcelable>("value"))
-                .isEqualTo(mySparseParcelableArray)
-        }
+        SparseParcelableArrayContainer(mySparseParcelableArray)
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                        decoded.value.contentEquals(original.value)
+                    } else {
+                        error("VERSION.SDK_INT < S")
+                    }
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getSparseParcelableArray<Parcelable>("value"))
+                        .isEqualTo(mySparseParcelableArray)
+                }
+            )
     }
 
     @Test
     fun collectionTypesWithConcreteElement() {
+        @Suppress("ArrayInDataClass")
         @Serializable
         data class CharSequenceArrayContainer(
-            @Serializable(with = CharSequenceArraySerializer::class) val value: Array<String>
-        ) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as CharSequenceArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
+            @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+            @Serializable(with = CharSequenceArraySerializer::class)
+            val value: Array<@Serializable(with = CharSequenceSerializer::class) StringBuilder>
+        )
+        val myCharSequenceArray = arrayOf<StringBuilder>(StringBuilder("foo"), StringBuilder("bar"))
+        // `Bundle.getCharSequenceArray()` returns a `CharSequence[]` and the actual element type
+        // is not being retained after parcel/unparcel so the plugin-generated serializer will
+        // get `ClassCastException` when trying to cast it back to `Array<StringBuilder>`.
+        assertThrows(ClassCastException::class) {
+            CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
+                assertThat(size()).isEqualTo(1)
+                assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
             }
         }
-        val myCharSequenceArray = arrayOf("foo", "bar")
-        CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
-        }
 
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+        @Suppress("ArrayInDataClass")
         @Serializable
         data class ParcelableArrayContainer(
             @Serializable(with = ParcelableArraySerializer::class)
             // Here the serializer for the element is actually not used, but leaving it out leads
             // to SERIALIZER_NOT_FOUND compile error.
-            val value: Array<@Serializable(with = ParcelableSerializer::class) MyParcelable>
-        ) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as ParcelableArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
-            }
-        }
+            val value:
+                Array<@Serializable(with = MyParcelableAsParcelableSerializer::class) MyParcelable>
+        )
         val myParcelableArray = arrayOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
-        ParcelableArrayContainer(myParcelableArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
+        // Even though `Bundle` does retain the actual `Parcelable` type there's no way for us to
+        // specify this `Parcelable` element type for the array, so the restored array is still of
+        // type `Array<Parcelable>` and the plugin-generated serializer will get
+        // `ClassCastException` when trying to cast it back to `Array<MyParcelable>`.
+        assertThrows(ClassCastException::class) {
+            ParcelableArrayContainer(myParcelableArray).encodeDecode {
+                assertThat(size()).isEqualTo(1)
+                assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
+            }
         }
 
         @Serializable
         data class CharSequenceListContainer(
-            @Serializable(with = CharSequenceListSerializer::class) val value: List<String>
+            @Serializable(with = CharSequenceListSerializer::class)
+            @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+            val value: List<@Serializable(with = CharSequenceSerializer::class) StringBuilder>
         )
-        val myCharSequenceList = arrayListOf("foo", "bar")
-        CharSequenceListContainer(myCharSequenceList).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequenceList("value")).isEqualTo(myCharSequenceList)
-        }
+        val myCharSequenceList = arrayListOf(StringBuilder("foo"), StringBuilder("bar"))
 
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+        CharSequenceListContainer(myCharSequenceList)
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    assertThat(original.value[0]::class).isEqualTo(StringBuilder::class)
+                    // This is similar to the `CharSequenceArray` case where the element type of the
+                    // restored List after parcel/unparcel is of `String` instead of
+                    // `StringBuilder`. However, since the element type of Lists is erased no
+                    // `CastCastException` is thrown when the plugin-generated serializer tried to
+                    // assign the restored list back to `List<StringBuilder>`.
+                    assertThat(decoded.value[0]::class).isEqualTo(String::class)
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getCharSequenceList("value")).isEqualTo(myCharSequenceList)
+                }
+            )
+
         @Serializable
         data class ParcelableListContainer(
+            // Unlike arrays this works as `List`s can be down-casted, e.g.
+            // a `List<Parcelable>` can be casted to `List<MyParcelable>`.
             @Serializable(with = ParcelableListSerializer::class)
-            val value: List<@Serializable(with = ParcelableSerializer::class) MyParcelable>
+            val value:
+                List<@Serializable(with = MyParcelableAsParcelableSerializer::class) MyParcelable>
         )
         val myParcelableList =
             arrayListOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
@@ -484,57 +466,37 @@
             assertThat(getParcelableList<MyParcelable>("value")).isEqualTo(myParcelableList)
         }
 
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
         @Serializable
         data class SparseParcelableArrayContainer(
+            // Unlike arrays this works as `SparseArray`s can be down-casted, e.g.
+            // a `SparseArray<Parcelable>` can be casted to `SparseArray<MyParcelable>`.
             @Serializable(with = SparseParcelableArraySerializer::class)
-            val value: SparseArray<@Serializable(with = ParcelableSerializer::class) MyParcelable>
+            val value:
+                SparseArray<
+                    @Serializable(with = MyParcelableAsParcelableSerializer::class)
+                    MyParcelable
+                >
         )
         val mySparseParcelableArray =
             SparseArray<MyParcelable>().apply {
                 append(1, MyParcelable(3, "foo", 3.14))
                 append(3, MyParcelable(4, "bar", 1.73))
             }
-        SparseParcelableArrayContainer(mySparseParcelableArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getSparseParcelableArray<Parcelable>("value"))
-                .isEqualTo(mySparseParcelableArray)
-        }
-    }
-
-    @Test
-    fun concreteTypeSerializers() {
-        // No need to suppress SERIALIZER_TYPE_INCOMPATIBLE with these serializers.
-        @Serializable
-        data class CharSequenceContainer(
-            @Serializable(with = StringAsCharSequenceSerializer::class) val value: String
-        )
-        CharSequenceContainer("foo").encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequence("value")).isEqualTo("foo")
-        }
-
-        @Serializable
-        data class SerializableContainer(
-            @Serializable(with = MyJavaSerializableAsJavaSerializableSerializer::class)
-            val value: MyJavaSerializable
-        )
-        val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
-        SerializableContainer(myJavaSerializable).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getJavaSerializable<MyJavaSerializable>("value"))
-                .isEqualTo(myJavaSerializable)
-        }
-
-        @Serializable
-        data class ParcelableContainer(
-            @Serializable(with = MyParcelableAsParcelableSerializer::class) val value: MyParcelable
-        )
-        val myParcelable = MyParcelable(3, "foo", 3.14)
-        ParcelableContainer(myParcelable).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getParcelable<MyParcelable>("value")).isEqualTo(myParcelable)
-        }
+        SparseParcelableArrayContainer(mySparseParcelableArray)
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                        assertThat(decoded.value.contentEquals(original.value))
+                    } else {
+                        error("VERSION.SDK_INT < S")
+                    }
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getSparseParcelableArray<Parcelable>("value"))
+                        .isEqualTo(mySparseParcelableArray)
+                }
+            )
     }
 }
 
@@ -610,45 +572,11 @@
     }
 }
 
-private object CharArrayAsStringSerializer : KSerializer<Array<Char>> {
-    private val delegateSerializer = serializer<String>()
-    override val descriptor: SerialDescriptor =
-        PrimitiveSerialDescriptor("Array<Char>", PrimitiveKind.STRING)
-
-    override fun deserialize(decoder: Decoder): Array<Char> {
-        val s = decoder.decodeSerializableValue(delegateSerializer)
-        val result = Array(s.length) { s[it] }
-        return result
-    }
-
-    override fun serialize(encoder: Encoder, value: Array<Char>) {
-        val charArray = CharArray(value.size)
-        value.forEachIndexed { index, c -> charArray[index] = c }
-        encoder.encodeSerializableValue(delegateSerializer, String(charArray))
-    }
-}
-
-@OptIn(ExperimentalSerializationApi::class)
-private object SparseStringArrayAsMapSerializer : KSerializer<SparseArray<String>> {
-    private val delegateSerializer = serializer<Map<Int, String>>()
-    override val descriptor = SerialDescriptor("SparseArray<String>", delegateSerializer.descriptor)
-
-    override fun deserialize(decoder: Decoder): SparseArray<String> {
-        val m = decoder.decodeSerializableValue(delegateSerializer)
-        val result = SparseArray<String>()
-        m.forEach { (k, v) -> result.append(k, v) }
-        return result
-    }
-
-    override fun serialize(encoder: Encoder, value: SparseArray<String>) {
-        val map = buildMap { value.forEach { k, v -> put(k, v) } }
-        encoder.encodeSerializableValue(delegateSerializer, map)
-    }
-}
-
-private class StringAsCharSequenceSerializer : CharSequenceSerializer<String>()
-
 private class MyJavaSerializableAsJavaSerializableSerializer :
     JavaSerializableSerializer<MyJavaSerializable>()
 
 private class MyParcelableAsParcelableSerializer : ParcelableSerializer<MyParcelable>()
+
+private class CustomJavaSerializableSerializer : JavaSerializableSerializer<java.io.Serializable>()
+
+private class CustomParcelableSerializer : ParcelableSerializer<Parcelable>()
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
new file mode 100644
index 0000000..edf79cc
--- /dev/null
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.savedstate
+
+import android.os.Parcel
+
+actual fun platformEncodeDecode(savedState: SavedState): SavedState {
+    val parcel =
+        Parcel.obtain().apply {
+            savedState.writeToParcel(this, 0)
+            setDataPosition(0)
+        }
+    return SavedState.CREATOR.createFromParcel(parcel)
+}
diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
index 65ccd1f..3f06dff 100644
--- a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
+++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
@@ -22,6 +22,7 @@
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.SerializationException
 import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.StructureKind
 import kotlinx.serialization.encoding.AbstractDecoder
 import kotlinx.serialization.encoding.CompositeDecoder
 import kotlinx.serialization.modules.EmptySerializersModule
@@ -73,9 +74,31 @@
     private var index = 0
 
     override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
-        if (index == savedState.read { size() }) return CompositeDecoder.DECODE_DONE
-        key = descriptor.getElementName(index)
-        return index++
+        val size =
+            if (descriptor.kind == StructureKind.LIST || descriptor.kind == StructureKind.MAP) {
+                // Use the number of elements encoded for collections.
+                savedState.read { size() }
+            } else {
+                // We may skip elements when encoding so if we used `size()`
+                // here we may miss some fields.
+                descriptor.elementsCount
+            }
+        fun hasDefaultValueDefined(index: Int) = descriptor.isElementOptional(index)
+        fun presentInEncoding(index: Int) =
+            savedState.read {
+                val key = descriptor.getElementName(index)
+                contains(key)
+            }
+        // Skip elements omitted from encoding (those assigned with its default values).
+        while (index < size && hasDefaultValueDefined(index) && !presentInEncoding(index)) {
+            index++
+        }
+        if (index < size) {
+            key = descriptor.getElementName(index)
+            return index++
+        } else {
+            return CompositeDecoder.DECODE_DONE
+        }
     }
 
     override fun decodeBoolean(): Boolean = savedState.read { getBoolean(key) }
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
index 86adf0b..88c33e4 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
@@ -401,15 +401,19 @@
 
     @Test
     fun sealedClasses() {
-        Node.Add(Node.Operand(3), Node.Operand(5)).encodeDecode {
+        // Should use base type for encoding/decoding.
+        Node.Add(Node.Operand(3), Node.Operand(5)).encodeDecode<Node> {
             assertThat(size()).isEqualTo(2)
-            getSavedState("lhs").read {
-                assertThat(size()).isEqualTo(1)
-                assertThat(getInt("value")).isEqualTo(3)
-            }
-            getSavedState("rhs").read {
-                assertThat(size()).isEqualTo(1)
-                assertThat(getInt("value")).isEqualTo(5)
+            assertThat(getString("type")).isEqualTo("androidx.savedstate.Node.Add")
+            getSavedState("value").read {
+                getSavedState("lhs").read {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getInt("value")).isEqualTo(3)
+                }
+                getSavedState("rhs").read {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getInt("value")).isEqualTo(5)
+                }
             }
         }
     }
@@ -425,12 +429,24 @@
         }
 
         // Nullable with default value.
-        @Serializable data class B(val s: String? = "foo")
-        B().encodeDecode()
-        B(s = "bar").encodeDecode {
+        @Serializable data class B(val s: String? = "foo", val i: Int)
+        B(i = 3).encodeDecode {
             assertThat(size()).isEqualTo(1)
+            assertThat(getInt("i")).isEqualTo(3)
+        }
+        B(s = null, i = 3).encodeDecode {
+            assertThat(size()).isEqualTo(2)
+            assertThat(isNull("s")).isTrue()
+        }
+        B(s = "bar", i = 3).encodeDecode {
+            assertThat(size()).isEqualTo(2)
             assertThat(getString("s")).isEqualTo("bar")
         }
+        // The value of `s` is the same as its default value so it's omitted from encoding.
+        B(s = "foo", i = 3).encodeDecode {
+            assertThat(size()).isEqualTo(1)
+            assertThat(getInt("i")).isEqualTo(3)
+        }
 
         // Nullable without default value
         @Serializable data class C(val s: String?)
@@ -450,6 +466,22 @@
             assertThat(getInt("i")).isEqualTo(5)
             assertThat(getString("s")).isEqualTo("foo")
         }
+
+        // Nullable with null as default value.
+        @Serializable data class E(val s: String? = null)
+        // Even though we encode `null`s in general as we don't encode default values
+        // nothing is encoded.
+        E().encodeDecode()
+
+        // Nullable in parent
+        G(i = 3).encodeDecode<F> {
+            assertThat(size()).isEqualTo(2)
+            assertThat(getString("type")).isEqualTo("androidx.savedstate.G")
+            getSavedState("value").read {
+                assertThat(size()).isEqualTo(1)
+                assertThat(getInt("i")).isEqualTo(3)
+            }
+        }
     }
 
     @Test
@@ -464,19 +496,24 @@
                     putSavedState("ss", savedState { putString("s", "bar") })
                 }
             )
-            .encodeDecode {
-                assertThat(size()).isEqualTo(1)
-                getSavedState("s").read {
-                    assertThat(size()).isEqualTo(4)
-                    assertThat(getInt("i")).isEqualTo(1)
-                    assertThat(getString("s")).isEqualTo("foo")
-                    assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
-                    getSavedState("ss").read {
-                        assertThat(size()).isEqualTo(1)
-                        assertThat(getString("s")).isEqualTo("bar")
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    assertThat(decoded.s.read { contentDeepEquals(original.s) })
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    getSavedState("s").read {
+                        assertThat(size()).isEqualTo(4)
+                        assertThat(getInt("i")).isEqualTo(1)
+                        assertThat(getString("s")).isEqualTo("foo")
+                        assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
+                        getSavedState("ss").read {
+                            assertThat(size()).isEqualTo(1)
+                            assertThat(getString("s")).isEqualTo("bar")
+                        }
                     }
                 }
-            }
+            )
 
         val origin = savedState {
             putInt("i", 1)
@@ -563,6 +600,7 @@
 
 private typealias MyNestedTypeAlias = MyTypeAliasToInt
 
+@Serializable
 private sealed class Node {
     @Serializable data class Add(val lhs: Operand, val rhs: Operand) : Node()
 
@@ -605,3 +643,10 @@
         return MyColor(array[0], array[1], array[2])
     }
 }
+
+@Serializable
+private sealed class F {
+    val s: String? = null
+}
+
+@Serializable private data class G(val i: Int) : F()
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
index e9ac161..67930cd 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
@@ -23,16 +23,30 @@
 import kotlinx.serialization.serializer
 
 internal object SavedStateCodecTestUtils {
+    /* Test the following steps: 1. encode `T` to a `SavedState`, 2. parcelize it to a `Parcel`,
+     * 3. un-parcelize it back to a `SavedState`, and 4. decode it back to a `T`. Step 2 and 3
+     * are only performed on Android. Here's the whole process:
+     *
+     * (A)Serializable -1-> (B)SavedState -2-> (C)Parcel -3-> (D)SavedState -4-> (E)Serializable
+     *
+     * `checkEncoded` can be used to check the content of "B", and `checkDecoded` can be
+     *  used to compare the instances of "E" and "A".
+     */
     inline fun <reified T : Any> T.encodeDecode(
         serializer: KSerializer<T> = serializer<T>(),
-        checkContent: SavedStateReader.() -> Unit = { assertThat(size()).isEqualTo(0) }
+        checkDecoded: (T, T) -> Unit = { decoded, original ->
+            assertThat(decoded).isEqualTo(original)
+        },
+        checkEncoded: SavedStateReader.() -> Unit = { assertThat(size()).isEqualTo(0) }
     ) {
-        assertThat(
-                decodeFromSavedState(
-                    serializer,
-                    encodeToSavedState(serializer, this).apply { read { checkContent() } }
-                )
-            )
-            .isEqualTo(this)
+        val encoded = encodeToSavedState(serializer, this)
+        encoded.read { checkEncoded() }
+
+        val restored = platformEncodeDecode(encoded)
+
+        val decoded = decodeFromSavedState(serializer, restored)
+        checkDecoded(decoded, this)
     }
 }
+
+expect fun platformEncodeDecode(savedState: SavedState): SavedState
diff --git a/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt
new file mode 100644
index 0000000..023701a8
--- /dev/null
+++ b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.savedstate
+
+// No parceling in non-Android platforms.
+actual fun platformEncodeDecode(savedState: SavedState): SavedState = savedState
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 8426c60..efb8ff6 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
     defaultConfig {
         applicationId = "androidx.wear.compose.integration.demos"
         minSdk = 25
-        versionCode = 63
-        versionName = "1.63"
+        versionCode = 64
+        versionName = "1.64"
     }
 
     buildTypes {
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index 8942afc..eae2d58 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -204,17 +204,6 @@
     method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
   }
 
-  public final class EdgeButtonStyle {
-    field public static final androidx.wear.protolayout.material3.EdgeButtonStyle.Companion Companion;
-    field public static final androidx.wear.protolayout.material3.EdgeButtonStyle DEFAULT;
-    field public static final androidx.wear.protolayout.material3.EdgeButtonStyle TOP_ALIGN;
-  }
-
-  public static final class EdgeButtonStyle.Companion {
-    property public final androidx.wear.protolayout.material3.EdgeButtonStyle DEFAULT;
-    property public final androidx.wear.protolayout.material3.EdgeButtonStyle TOP_ALIGN;
-  }
-
   public final class GraphicDataCardStyle {
     field public static final androidx.wear.protolayout.material3.GraphicDataCardStyle.Companion Companion;
   }
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index 8942afc..eae2d58 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -204,17 +204,6 @@
     method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
   }
 
-  public final class EdgeButtonStyle {
-    field public static final androidx.wear.protolayout.material3.EdgeButtonStyle.Companion Companion;
-    field public static final androidx.wear.protolayout.material3.EdgeButtonStyle DEFAULT;
-    field public static final androidx.wear.protolayout.material3.EdgeButtonStyle TOP_ALIGN;
-  }
-
-  public static final class EdgeButtonStyle.Companion {
-    property public final androidx.wear.protolayout.material3.EdgeButtonStyle DEFAULT;
-    property public final androidx.wear.protolayout.material3.EdgeButtonStyle TOP_ALIGN;
-  }
-
   public final class GraphicDataCardStyle {
     field public static final androidx.wear.protolayout.material3.GraphicDataCardStyle.Companion Companion;
   }
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
index 6b02da5..4537676 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
@@ -83,14 +83,17 @@
         testCases["primarylayout_edgebuttonfilled_buttongroup_iconoverride_golden$goldenSuffix"] =
             materialScope(
                 ApplicationProvider.getApplicationContext(),
-                deviceParameters,
+                // renderer version 1.302 has no asymmetrical corner support, so edgebutton will use
+                // its fallback style
+                deviceParameters.copy(VersionInfo.Builder().setMajor(1).setMinor(302).build()),
                 allowDynamicTheme = false
             ) {
                 primaryLayoutWithOverrideIcon(
                     mainSlot = {
                         text(
-                            text = "Text in the main slot that overflows".layoutString,
-                            color = colorScheme.secondary
+                            text = "Overflow main text and fallback edge button".layoutString,
+                            color = colorScheme.secondary,
+                            maxLines = 3
                         )
                     },
                     bottomSlot = {
@@ -476,11 +479,23 @@
         testCases["primarylayout_circularprogressindicators_fallback__golden$NORMAL_SCALE_SUFFIX"] =
             materialScope(
                 ApplicationProvider.getApplicationContext(),
+                // renderer with version 1.302 has no DashedArcLine or asymmetrical corners support
                 deviceConfiguration =
-                    deviceParameters.copy(VersionInfo.Builder().setMajor(1).setMinor(402).build()),
+                    deviceParameters.copy(VersionInfo.Builder().setMajor(1).setMinor(302).build()),
                 allowDynamicTheme = false
             ) {
-                primaryLayout(mainSlot = { progressIndicatorGroup() })
+                primaryLayout(
+                    mainSlot = { progressIndicatorGroup() },
+                    bottomSlot = {
+                        iconEdgeButton(
+                            onClick = clickable,
+                            iconContent = { icon(ICON_ID) },
+                            modifier =
+                                LayoutModifier.contentDescription(CONTENT_DESCRIPTION_PLACEHOLDER),
+                            colors = filledTonalButtonColors()
+                        )
+                    }
+                )
             }
 
         return collectTestCases(testCases)
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
index e503797..5c438b7 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
@@ -27,8 +27,10 @@
 import androidx.wear.protolayout.ModifiersBuilders.Clickable
 import androidx.wear.protolayout.ModifiersBuilders.Padding
 import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
 import androidx.wear.protolayout.material3.ButtonDefaults.filledButtonColors
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
+import androidx.wear.protolayout.material3.EdgeButtonDefaults.CONTAINER_HEIGHT_DP
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.HORIZONTAL_MARGIN_PERCENT_LARGE
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.HORIZONTAL_MARGIN_PERCENT_SMALL
@@ -37,8 +39,16 @@
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.TEXT_SIDE_PADDING_DP
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.TEXT_TOP_PADDING_DP
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.TOP_CORNER_RADIUS
-import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.DEFAULT
-import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TOP_ALIGN
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.BOTTOM_MARGIN_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.CORNER_RADIUS_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.EDGE_BUTTON_HEIGHT_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.ICON_SIDE_PADDING_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.ICON_SIZE_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.TEXT_SIDE_PADDING_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.ICON
+import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.ICON_FALLBACK
+import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TEXT
+import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TEXT_FALLBACK
 import androidx.wear.protolayout.modifiers.LayoutModifier
 import androidx.wear.protolayout.modifiers.background
 import androidx.wear.protolayout.modifiers.clickable
@@ -50,6 +60,7 @@
 import androidx.wear.protolayout.modifiers.semanticsRole
 import androidx.wear.protolayout.modifiers.tag
 import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
+import androidx.wear.protolayout.types.dp
 
 /**
  * ProtoLayout Material3 component edge button that offers a single slot to take an icon or similar
@@ -82,14 +93,22 @@
     modifier: LayoutModifier = LayoutModifier,
     colors: ButtonColors = filledButtonColors(),
     iconContent: (MaterialScope.() -> LayoutElement)
-): LayoutElement =
-    edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = DEFAULT) {
+): LayoutElement {
+    val style =
+        if (deviceConfiguration.rendererSchemaVersion.hasAsymmetricalCornersSupport()) {
+            ICON
+        } else {
+            ICON_FALLBACK
+        }
+
+    return edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = style) {
         withStyle(
                 defaultIconStyle =
-                    IconStyle(size = ICON_SIZE_DP.toDp(), tintColor = colors.iconColor)
+                    IconStyle(size = style.iconSizeDp.dp, tintColor = colors.iconColor)
             )
             .iconContent()
     }
+}
 
 /**
  * ProtoLayout Material3 component edge button that offers a single slot to take a text or similar
@@ -123,7 +142,17 @@
     colors: ButtonColors = filledButtonColors(),
     labelContent: (MaterialScope.() -> LayoutElement)
 ): LayoutElement =
-    edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = TOP_ALIGN) {
+    edgeButton(
+        onClick = onClick,
+        modifier = modifier,
+        colors = colors,
+        style =
+            if (deviceConfiguration.rendererSchemaVersion.hasAsymmetricalCornersSupport()) {
+                TEXT
+            } else {
+                TEXT_FALLBACK
+            }
+    ) {
         withStyle(
                 defaultTextElementStyle =
                     TextElementStyle(
@@ -156,8 +185,8 @@
  * @param modifier Modifiers to set to this element. It's highly recommended to set a content
  *   description using [contentDescription].
  * @param style The style used for the inner content, specifying how the content should be aligned.
- *   It is recommended to use [EdgeButtonStyle.TOP_ALIGN] for long, wide content. If not set,
- *   defaults to [EdgeButtonStyle.DEFAULT] which center-aligns the content.
+ *   It is recommended to use [EdgeButtonStyle.TEXT] for long, wide content. If not set, defaults to
+ *   [EdgeButtonStyle.ICON] which center-aligns the content.
  * @param content The inner content to be put inside of this edge button.
  * @sample androidx.wear.protolayout.material3.samples.edgeButtonSampleIcon
  */
@@ -166,7 +195,7 @@
     onClick: Clickable,
     colors: ButtonColors,
     modifier: LayoutModifier = LayoutModifier,
-    style: EdgeButtonStyle = DEFAULT,
+    style: EdgeButtonStyle = ICON,
     content: MaterialScope.() -> LayoutElement
 ): LayoutElement {
     val containerWidth = deviceConfiguration.screenWidthDp.toDp()
@@ -175,50 +204,83 @@
         else HORIZONTAL_MARGIN_PERCENT_SMALL
     val edgeButtonWidth: Float =
         (100f - 2f * horizontalMarginPercent) * deviceConfiguration.screenWidthDp / 100f
-    val bottomCornerRadiusX = edgeButtonWidth / 2f
-    val bottomCornerRadiusY = EDGE_BUTTON_HEIGHT_DP - TOP_CORNER_RADIUS
 
     var mod =
         (LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier)
             .clickable(onClick)
             .background(colors.containerColor)
-            .clip(TOP_CORNER_RADIUS)
-            .clipBottomLeft(bottomCornerRadiusX, bottomCornerRadiusY)
-            .clipBottomRight(bottomCornerRadiusX, bottomCornerRadiusY)
+            .clip(style.topCornerRadiusDp)
+
+    if (deviceConfiguration.rendererSchemaVersion.hasAsymmetricalCornersSupport()) {
+        val bottomCornerRadiusX = edgeButtonWidth / 2f
+        val bottomCornerRadiusY = style.buttonHeightDp - style.topCornerRadiusDp
+        mod =
+            mod.clipBottomLeft(bottomCornerRadiusX, bottomCornerRadiusY)
+                .clipBottomRight(bottomCornerRadiusX, bottomCornerRadiusY)
+    }
 
     style.padding?.let { mod = mod.padding(it) }
 
-    val button = Box.Builder().setHeight(EDGE_BUTTON_HEIGHT_DP.toDp()).setWidth(dp(edgeButtonWidth))
-    button
-        .setVerticalAlignment(style.verticalAlignment)
-        .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
-        .addContent(content())
+    val button =
+        Box.Builder()
+            .setHeight(style.buttonHeightDp.dp)
+            .setWidth(dp(edgeButtonWidth))
+            .setVerticalAlignment(style.verticalAlignment)
+            .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
+            .addContent(content())
+            .setModifiers(mod.toProtoLayoutModifiers())
+            .build()
 
     return Box.Builder()
-        .setHeight((EDGE_BUTTON_HEIGHT_DP + BOTTOM_MARGIN_DP).toDp())
+        .setHeight(CONTAINER_HEIGHT_DP.dp)
         .setWidth(containerWidth)
-        .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_TOP)
+        .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM)
         .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
-        .addContent(button.setModifiers(mod.toProtoLayoutModifiers()).build())
-        .setModifiers(LayoutModifier.tag(METADATA_TAG).toProtoLayoutModifiers())
+        .addContent(button)
+        .setModifiers(
+            LayoutModifier.tag(METADATA_TAG)
+                .padding(padding(bottom = style.bottomMarginDp.toFloat()))
+                .toProtoLayoutModifiers()
+        )
         .build()
 }
 
-/** Provides style values for edge button component. */
-public class EdgeButtonStyle
-private constructor(
+/**
+ * Provides style values for edge button component.
+ *
+ * An [edgeButton] has a wrapper container with the screen width, and fixed height of
+ * [CONTAINER_HEIGHT_DP].
+ *
+ * The visible button box has height of [buttonHeightDp], it is centered horizontally in side the
+ * wrapper container with [HORIZONTAL_MARGIN_PERCENT_SMALL] or [HORIZONTAL_MARGIN_PERCENT_LARGE]
+ * depending on the screen size. It is then horizontally aligned to the bottom with bottom margin of
+ * [bottomMarginDp].
+ *
+ * The visible button box has its top two corners clipped with the [topCornerRadiusDp], while its
+ * bottom two corners will be clipped fully both horizontally and vertically to achieving the edge
+ * hugging shape. In the fallback implementation, without asymmetrical corners support, the
+ * [topCornerRadiusDp] is applied to all four corners.
+ *
+ * The content (icon or text) of the button is located inside the visible button box with [padding],
+ * horizontally centered and vertically aligned with the given [verticalAlignment].
+ */
+internal class EdgeButtonStyle
+internal constructor(
     @VerticalAlignment internal val verticalAlignment: Int = VERTICAL_ALIGN_CENTER,
-    internal val padding: Padding? = null
+    internal val padding: Padding? = null,
+    @Dimension(DP) internal val buttonHeightDp: Float = EDGE_BUTTON_HEIGHT_DP,
+    @Dimension(DP) internal val bottomMarginDp: Float = BOTTOM_MARGIN_DP,
+    @Dimension(DP) internal val iconSizeDp: Float = ICON_SIZE_DP,
+    @Dimension(DP) internal val topCornerRadiusDp: Float = TOP_CORNER_RADIUS
 ) {
-    public companion object {
+    internal companion object {
         /**
-         * Style variation for having content of the edge button anchored to the top.
+         * Style variation for having text content with edge hugging shape.
          *
-         * This should be used for text-like content, or the content that is wide, to accommodate
-         * for more space.
+         * The text is vertically aligned to top with [TEXT_TOP_PADDING_DP] to to accommodate for
+         * more horizontal space.
          */
-        @JvmField
-        public val TOP_ALIGN: EdgeButtonStyle =
+        internal val TEXT: EdgeButtonStyle =
             EdgeButtonStyle(
                 verticalAlignment = LayoutElementBuilders.VERTICAL_ALIGN_TOP,
                 padding =
@@ -230,28 +292,101 @@
             )
 
         /**
-         * Default style variation for having content of the edge button center aligned.
+         * Style variation for having icon content with edge hugging shape.
          *
-         * This should be used for icon-like or small, round content that doesn't occupy a lot of
-         * space.
+         * The icon is centered in the visible button box with the size of [ICON_SIZE_DP].
          */
-        @JvmField public val DEFAULT: EdgeButtonStyle = EdgeButtonStyle()
+        internal val ICON: EdgeButtonStyle = EdgeButtonStyle()
+
+        /**
+         * Style variation for fallback implementation with text content, when there is no
+         * asymmetrical corners support.
+         *
+         * Without the edge hugging shape, the [topCornerRadius] value is a full cornered value and
+         * is applied to all four corners. To avoid being clipped by the screen edge, the visible
+         * button box is pushed upwards with a bigger bottom margin of [BOTTOM_MARGIN_FALLBACK_DP].
+         * Also the box height shrinks to [EDGE_BUTTON_HEIGHT_FALLBACK_DP].
+         *
+         * Its text content is center placed with a increased horizontal padding of
+         * [TEXT_SIDE_PADDING_FALLBACK_DP].
+         */
+        internal val TEXT_FALLBACK: EdgeButtonStyle =
+            EdgeButtonStyle(
+                verticalAlignment = VERTICAL_ALIGN_CENTER,
+                padding =
+                    padding(
+                        start = TEXT_SIDE_PADDING_FALLBACK_DP,
+                        end = TEXT_SIDE_PADDING_FALLBACK_DP,
+                    ),
+                buttonHeightDp = EDGE_BUTTON_HEIGHT_FALLBACK_DP,
+                topCornerRadiusDp = CORNER_RADIUS_FALLBACK_DP,
+                bottomMarginDp = BOTTOM_MARGIN_FALLBACK_DP
+            )
+
+        /**
+         * Style variation for fallback implementation with icon content, when there is no
+         * asymmetrical corners support.
+         *
+         * Without the edge hugging shape, the [topCornerRadius] value is a full cornered value and
+         * is applied to all four corners. To avoid being clipped by the screen, the visible button
+         * box is pushed upwards with a bigger bottom margin of [BOTTOM_MARGIN_FALLBACK_DP]. Also
+         * the box height shrinks to [EDGE_BUTTON_HEIGHT_FALLBACK_DP]
+         *
+         * Its icon content center placed with increased horizontal padding
+         * [ICON_SIDE_PADDING_FALLBACK_DP]. Also, the icon size is also increased to
+         * [ICON_SIZE_FALLBACK_DP].
+         */
+        internal val ICON_FALLBACK: EdgeButtonStyle =
+            EdgeButtonStyle(
+                verticalAlignment = VERTICAL_ALIGN_CENTER,
+                padding =
+                    padding(
+                        start = ICON_SIDE_PADDING_FALLBACK_DP,
+                        end = ICON_SIDE_PADDING_FALLBACK_DP,
+                    ),
+                buttonHeightDp = EDGE_BUTTON_HEIGHT_FALLBACK_DP,
+                topCornerRadiusDp = CORNER_RADIUS_FALLBACK_DP,
+                bottomMarginDp = BOTTOM_MARGIN_FALLBACK_DP,
+                iconSizeDp = ICON_SIZE_FALLBACK_DP
+            )
     }
 }
 
 internal object EdgeButtonDefaults {
-    @Dimension(DP) internal const val TOP_CORNER_RADIUS: Float = 17f
+    @Dimension(DP) internal const val TOP_CORNER_RADIUS = 17f
     /** The horizontal margin used for width of the EdgeButton, below the 225dp breakpoint. */
-    internal const val HORIZONTAL_MARGIN_PERCENT_SMALL: Float = 24f
+    internal const val HORIZONTAL_MARGIN_PERCENT_SMALL = 24f
     /** The horizontal margin used for width of the EdgeButton, above the 225dp breakpoint. */
-    internal const val HORIZONTAL_MARGIN_PERCENT_LARGE: Float = 26f
-    internal const val BOTTOM_MARGIN_DP: Int = 3
-    internal const val EDGE_BUTTON_HEIGHT_DP: Int = 46
-    internal const val METADATA_TAG: String = "EB"
-    internal const val ICON_SIZE_DP = 24
-    internal const val TEXT_TOP_PADDING_DP = 12f
-    internal const val TEXT_SIDE_PADDING_DP = 8f
+    internal const val HORIZONTAL_MARGIN_PERCENT_LARGE = 26f
+    @Dimension(DP) internal const val BOTTOM_MARGIN_DP = 3f
+    @Dimension(DP) internal const val EDGE_BUTTON_HEIGHT_DP = 46f
+    @Dimension(DP) internal const val CONTAINER_HEIGHT_DP = EDGE_BUTTON_HEIGHT_DP + BOTTOM_MARGIN_DP
+    internal const val METADATA_TAG = "EB"
+    @Dimension(DP) internal const val ICON_SIZE_DP = 24f
+    @Dimension(DP) internal const val TEXT_TOP_PADDING_DP = 12f
+    @Dimension(DP) internal const val TEXT_SIDE_PADDING_DP = 8f
+}
+
+/**
+ * This object provides constants and styles of the fallback layout for [iconEdgeButton] and
+ * [textEdgeButton] when the renderer version is lower than 1.3.3 where asymmetrical corners support
+ * is not available.
+ */
+internal object EdgeButtonFallbackDefaults {
+    @Dimension(DP) internal const val ICON_SIZE_FALLBACK_DP = 26f
+    @Dimension(DP) internal const val EDGE_BUTTON_HEIGHT_FALLBACK_DP = 40f
+    @Dimension(DP) internal const val BOTTOM_MARGIN_FALLBACK_DP = 7f
+    @Dimension(DP)
+    internal const val CORNER_RADIUS_FALLBACK_DP = EDGE_BUTTON_HEIGHT_FALLBACK_DP / 2f
+    @Dimension(DP) internal const val TEXT_SIDE_PADDING_FALLBACK_DP = 14f
+    @Dimension(DP) internal const val ICON_SIDE_PADDING_FALLBACK_DP = 20f
 }
 
 internal fun LayoutElement.isSlotEdgeButton(): Boolean =
     this is Box && METADATA_TAG == this.modifiers?.metadata?.toTagName()
+
+/**
+ * Checks whether the renderer has support for asymmetrical corners, which is added in version
+ * 1.303.
+ */
+private fun VersionInfo.hasAsymmetricalCornersSupport() = major > 1 || (major == 1 && minor >= 303)
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
index c8c40e5..15636ae 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
@@ -25,8 +25,11 @@
 import androidx.wear.protolayout.LayoutElementBuilders.Image
 import androidx.wear.protolayout.expression.AppDataKey
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
-import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
+import androidx.wear.protolayout.material3.EdgeButtonDefaults.CONTAINER_HEIGHT_DP
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.EDGE_BUTTON_HEIGHT_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.ICON_SIZE_FALLBACK_DP
 import androidx.wear.protolayout.modifiers.LayoutModifier
 import androidx.wear.protolayout.modifiers.clickable
 import androidx.wear.protolayout.modifiers.contentDescription
@@ -41,6 +44,7 @@
 import androidx.wear.protolayout.testing.isClickable
 import androidx.wear.protolayout.types.LayoutString
 import androidx.wear.protolayout.types.asLayoutConstraint
+import androidx.wear.protolayout.types.dp
 import androidx.wear.protolayout.types.layoutString
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,14 +58,14 @@
         LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
             .onRoot()
             .assert(hasWidth(DEVICE_CONFIGURATION.screenWidthDp.toDp()))
-            .assert(hasHeight((EDGE_BUTTON_HEIGHT_DP + BOTTOM_MARGIN_DP).toDp()))
+            .assert(hasHeight(CONTAINER_HEIGHT_DP.dp))
     }
 
     @Test
     fun visibleHeight() {
         LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
             .onElement(isClickable())
-            .assert(hasHeight(EDGE_BUTTON_HEIGHT_DP.toDp()))
+            .assert(hasHeight(EDGE_BUTTON_HEIGHT_DP.dp))
     }
 
     @Test
@@ -163,6 +167,29 @@
             .assert(hasColor(COLOR_SCHEME.onPrimary.staticArgb))
     }
 
+    @Test
+    fun containerFallbackSize() {
+        LayoutElementAssertionsProvider(ICON_EDGE_BUTTON_FALLBACK)
+            .onRoot()
+            .assert(hasWidth(DEVICE_CONFIGURATION.screenWidthDp.toDp()))
+            .assert(hasHeight(CONTAINER_HEIGHT_DP.dp))
+    }
+
+    @Test
+    fun visibleFallbackHeight() {
+        LayoutElementAssertionsProvider(ICON_EDGE_BUTTON_FALLBACK)
+            .onElement(isClickable())
+            .assert(hasHeight(EDGE_BUTTON_HEIGHT_FALLBACK_DP.dp))
+    }
+
+    @Test
+    fun iconFallbackSize() {
+        LayoutElementAssertionsProvider(ICON_EDGE_BUTTON_FALLBACK)
+            .onElement(hasImage(RES_ID))
+            .assert(hasWidth(ICON_SIZE_FALLBACK_DP.dp))
+            .assert(hasHeight(ICON_SIZE_FALLBACK_DP.dp))
+    }
+
     companion object {
         private val CONTEXT = getApplicationContext() as Context
         private val COLOR_SCHEME = ColorScheme()
@@ -171,6 +198,14 @@
             DeviceParametersBuilders.DeviceParameters.Builder()
                 .setScreenWidthDp(192)
                 .setScreenHeightDp(192)
+                .setRendererSchemaVersion(VersionInfo.Builder().setMajor(99).setMinor(999).build())
+                .build()
+
+        private val DEVICE_CONFIGURATION_WITH_OLD_RENDERER =
+            DeviceParametersBuilders.DeviceParameters.Builder()
+                .setScreenWidthDp(192)
+                .setScreenHeightDp(192)
+                .setRendererSchemaVersion(VersionInfo.Builder().setMajor(1).setMinor(302).build())
                 .build()
 
         private val CLICKABLE =
@@ -188,5 +223,18 @@
                     icon(RES_ID)
                 }
             }
+        private val ICON_EDGE_BUTTON_FALLBACK =
+            materialScope(
+                CONTEXT,
+                DEVICE_CONFIGURATION_WITH_OLD_RENDERER,
+                allowDynamicTheme = false
+            ) {
+                iconEdgeButton(
+                    onClick = CLICKABLE,
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+                ) {
+                    icon(RES_ID)
+                }
+            }
     }
 }
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
index 7e04e8a..edfbff0 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
@@ -155,6 +155,9 @@
   //
   // If not set, the animation will play on load.</setter>
   androidx.wear.protolayout.expression.proto.DynamicFloat progress = 2;
+
+  // The trigger to start the animation.
+  Trigger start_trigger = 3;
 }
 
 // An image resource, which can be used by layouts. This holds multiple
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index 21886f5..da4e766 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1309,12 +1309,14 @@
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public static final class ResourceBuilders.AndroidLottieResourceByResId {
     method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? getProgress();
     method @RawRes public int getRawResourceId();
+    method public androidx.wear.protolayout.TriggerBuilders.Trigger? getStartTrigger();
   }
 
   public static final class ResourceBuilders.AndroidLottieResourceByResId.Builder {
     ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public ResourceBuilders.AndroidLottieResourceByResId.Builder(@RawRes int);
     method public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setProgress(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setStartTrigger(androidx.wear.protolayout.TriggerBuilders.Trigger);
   }
 
   @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class ResourceBuilders.AndroidSeekableAnimatedImageResourceByResId {
@@ -1434,6 +1436,24 @@
   public final class TriggerBuilders {
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnConditionMetTrigger(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnLoadTrigger();
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleOnceTrigger();
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleTrigger();
+  }
+
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleOnceTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+  }
+
+  public static final class TriggerBuilders.OnVisibleOnceTrigger.Builder {
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleOnceTrigger.Builder();
+    method public androidx.wear.protolayout.TriggerBuilders.OnVisibleOnceTrigger build();
+  }
+
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+  }
+
+  public static final class TriggerBuilders.OnVisibleTrigger.Builder {
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleTrigger.Builder();
+    method public androidx.wear.protolayout.TriggerBuilders.OnVisibleTrigger build();
   }
 
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface TriggerBuilders.Trigger {
@@ -1527,6 +1547,10 @@
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
   }
 
+  public final class BorderKt {
+    method public static androidx.wear.protolayout.modifiers.LayoutModifier border(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float width, androidx.wear.protolayout.types.LayoutColor color);
+  }
+
   public final class ClickableKt {
     method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable();
     method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
@@ -1557,6 +1581,10 @@
     method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
   }
 
+  public final class OpacityKt {
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier opacity(androidx.wear.protolayout.modifiers.LayoutModifier, @FloatRange(from=0.0, to=1.0) float staticValue, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? dynamicValue);
+  }
+
   public final class PaddingKt {
     method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Padding padding);
     method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
@@ -1572,6 +1600,10 @@
     method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
   }
 
+  public final class VisibilityKt {
+    method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public static androidx.wear.protolayout.modifiers.LayoutModifier visibility(androidx.wear.protolayout.modifiers.LayoutModifier, boolean staticVisibility, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool? dynamicVisibility);
+  }
+
 }
 
 package androidx.wear.protolayout.types {
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index 21886f5..da4e766 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1309,12 +1309,14 @@
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public static final class ResourceBuilders.AndroidLottieResourceByResId {
     method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? getProgress();
     method @RawRes public int getRawResourceId();
+    method public androidx.wear.protolayout.TriggerBuilders.Trigger? getStartTrigger();
   }
 
   public static final class ResourceBuilders.AndroidLottieResourceByResId.Builder {
     ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public ResourceBuilders.AndroidLottieResourceByResId.Builder(@RawRes int);
     method public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setProgress(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setStartTrigger(androidx.wear.protolayout.TriggerBuilders.Trigger);
   }
 
   @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class ResourceBuilders.AndroidSeekableAnimatedImageResourceByResId {
@@ -1434,6 +1436,24 @@
   public final class TriggerBuilders {
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnConditionMetTrigger(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnLoadTrigger();
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleOnceTrigger();
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleTrigger();
+  }
+
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleOnceTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+  }
+
+  public static final class TriggerBuilders.OnVisibleOnceTrigger.Builder {
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleOnceTrigger.Builder();
+    method public androidx.wear.protolayout.TriggerBuilders.OnVisibleOnceTrigger build();
+  }
+
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+  }
+
+  public static final class TriggerBuilders.OnVisibleTrigger.Builder {
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleTrigger.Builder();
+    method public androidx.wear.protolayout.TriggerBuilders.OnVisibleTrigger build();
   }
 
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface TriggerBuilders.Trigger {
@@ -1527,6 +1547,10 @@
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
   }
 
+  public final class BorderKt {
+    method public static androidx.wear.protolayout.modifiers.LayoutModifier border(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float width, androidx.wear.protolayout.types.LayoutColor color);
+  }
+
   public final class ClickableKt {
     method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable();
     method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
@@ -1557,6 +1581,10 @@
     method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
   }
 
+  public final class OpacityKt {
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier opacity(androidx.wear.protolayout.modifiers.LayoutModifier, @FloatRange(from=0.0, to=1.0) float staticValue, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? dynamicValue);
+  }
+
   public final class PaddingKt {
     method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Padding padding);
     method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
@@ -1572,6 +1600,10 @@
     method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
   }
 
+  public final class VisibilityKt {
+    method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public static androidx.wear.protolayout.modifiers.LayoutModifier visibility(androidx.wear.protolayout.modifiers.LayoutModifier, boolean staticVisibility, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool? dynamicVisibility);
+  }
+
 }
 
 package androidx.wear.protolayout.types {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
index 0fff0a2..a51c996 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
@@ -520,6 +520,15 @@
             }
         }
 
+        /** Gets the trigger to start the animation. */
+        public @Nullable Trigger getStartTrigger() {
+            if (mImpl.hasStartTrigger()) {
+                return TriggerBuilders.triggerFromProto(mImpl.getStartTrigger());
+            } else {
+                return null;
+            }
+        }
+
         /** Creates a new wrapper instance from the proto. */
         @RestrictTo(Scope.LIBRARY_GROUP)
         public static @NonNull AndroidLottieResourceByResId fromProto(
@@ -540,6 +549,8 @@
                     + getRawResourceId()
                     + ", progress="
                     + getProgress()
+                    + ", startTrigger="
+                    + getStartTrigger()
                     + "}";
         }
 
@@ -590,6 +601,13 @@
                 return this;
             }
 
+            /** Sets the trigger to start the animation. */
+            @RequiresSchemaVersion(major = 1, minor = 500)
+            public @NonNull Builder setStartTrigger(@NonNull Trigger startTrigger) {
+                mImpl.setStartTrigger(startTrigger.toTriggerProto());
+                return this;
+            }
+
             /** Builds an instance from accumulated values. */
             public @NonNull AndroidLottieResourceByResId build() {
                 return AndroidLottieResourceByResId.fromProto(mImpl.build());
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
index 1a96d0e..d05a53c 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
@@ -48,6 +48,160 @@
         return new OnConditionMetTrigger.Builder().setCondition(dynamicBool).build();
     }
 
+    /**
+     * Creates a {@link Trigger} that fires *every time* the layout becomes visible.
+     *
+     * <p>As opposed to {@link #createOnLoadTrigger()}, this will wait until layout is fully visible
+     * before firing a trigger.
+     */
+    @RequiresSchemaVersion(major = 1, minor = 200)
+    public static @NonNull Trigger createOnVisibleTrigger() {
+        return new OnVisibleTrigger.Builder().build();
+    }
+
+    /**
+     * Creates a {@link Trigger} that fires the first time that layout becomes visible.
+     *
+     * <p>As opposed to {@link #createOnVisibleTrigger()}, this will only be fired the first time
+     * that the layout becomes visible.
+     */
+    @RequiresSchemaVersion(major = 1, minor = 200)
+    public static @NonNull Trigger createOnVisibleOnceTrigger() {
+        return new OnVisibleOnceTrigger.Builder().build();
+    }
+
+    /** Triggers when the layout visibility state turns from invisible to fully visible. */
+    @RequiresSchemaVersion(major = 1, minor = 200)
+    public static final class OnVisibleTrigger implements Trigger {
+        private final TriggerProto.OnVisibleTrigger mImpl;
+        private final @Nullable Fingerprint mFingerprint;
+
+        OnVisibleTrigger(TriggerProto.OnVisibleTrigger impl, @Nullable Fingerprint fingerprint) {
+            this.mImpl = impl;
+            this.mFingerprint = fingerprint;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public @Nullable Fingerprint getFingerprint() {
+            return mFingerprint;
+        }
+
+        /** Creates a new wrapper instance from the proto. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public static @NonNull OnVisibleTrigger fromProto(
+                TriggerProto.@NonNull OnVisibleTrigger proto, @Nullable Fingerprint fingerprint) {
+            return new OnVisibleTrigger(proto, fingerprint);
+        }
+
+        static @NonNull OnVisibleTrigger fromProto(TriggerProto.@NonNull OnVisibleTrigger proto) {
+            return fromProto(proto, null);
+        }
+
+        /** Returns the internal proto instance. */
+        TriggerProto.@NonNull OnVisibleTrigger toProto() {
+            return mImpl;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public TriggerProto.@NonNull Trigger toTriggerProto() {
+            return TriggerProto.Trigger.newBuilder().setOnVisibleTrigger(mImpl).build();
+        }
+
+        @Override
+        public @NonNull String toString() {
+            return "OnVisibleTrigger";
+        }
+
+        /** Builder for {@link OnVisibleTrigger}. */
+        @SuppressWarnings("HiddenSuperclass")
+        public static final class Builder implements Trigger.Builder {
+            private final TriggerProto.OnVisibleTrigger.Builder mImpl =
+                    TriggerProto.OnVisibleTrigger.newBuilder();
+            private final Fingerprint mFingerprint = new Fingerprint(1416366796);
+
+            /** Creates an instance of {@link Builder}. */
+            @RequiresSchemaVersion(major = 1, minor = 200)
+            public Builder() {}
+
+            /** Builds an instance from accumulated values. */
+            @Override
+            public @NonNull OnVisibleTrigger build() {
+                return new OnVisibleTrigger(mImpl.build(), mFingerprint);
+            }
+        }
+    }
+
+    /**
+     * Triggers only once when the layout visibility state turns from invisible to fully visible for
+     * the first time.
+     */
+    @RequiresSchemaVersion(major = 1, minor = 200)
+    public static final class OnVisibleOnceTrigger implements Trigger {
+        private final TriggerProto.OnVisibleOnceTrigger mImpl;
+        private final @Nullable Fingerprint mFingerprint;
+
+        OnVisibleOnceTrigger(
+                TriggerProto.OnVisibleOnceTrigger impl, @Nullable Fingerprint fingerprint) {
+            this.mImpl = impl;
+            this.mFingerprint = fingerprint;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public @Nullable Fingerprint getFingerprint() {
+            return mFingerprint;
+        }
+
+        /** Creates a new wrapper instance from the proto. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public static @NonNull OnVisibleOnceTrigger fromProto(
+                TriggerProto.@NonNull OnVisibleOnceTrigger proto,
+                @Nullable Fingerprint fingerprint) {
+            return new OnVisibleOnceTrigger(proto, fingerprint);
+        }
+
+        static @NonNull OnVisibleOnceTrigger fromProto(
+                TriggerProto.@NonNull OnVisibleOnceTrigger proto) {
+            return fromProto(proto, null);
+        }
+
+        /** Returns the internal proto instance. */
+        TriggerProto.@NonNull OnVisibleOnceTrigger toProto() {
+            return mImpl;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public TriggerProto.@NonNull Trigger toTriggerProto() {
+            return TriggerProto.Trigger.newBuilder().setOnVisibleOnceTrigger(mImpl).build();
+        }
+
+        @Override
+        public @NonNull String toString() {
+            return "OnVisibleOnceTrigger";
+        }
+
+        /** Builder for {@link OnVisibleOnceTrigger}. */
+        @SuppressWarnings("HiddenSuperclass")
+        public static final class Builder implements Trigger.Builder {
+            private final TriggerProto.OnVisibleOnceTrigger.Builder mImpl =
+                    TriggerProto.OnVisibleOnceTrigger.newBuilder();
+            private final Fingerprint mFingerprint = new Fingerprint(-1661457257);
+
+            /** Creates an instance of {@link Builder}. */
+            @RequiresSchemaVersion(major = 1, minor = 200)
+            public Builder() {}
+
+            /** Builds an instance from accumulated values. */
+            @Override
+            public @NonNull OnVisibleOnceTrigger build() {
+                return new OnVisibleOnceTrigger(mImpl.build(), mFingerprint);
+            }
+        }
+    }
+
     /** Triggers immediately when the layout is loaded / reloaded. */
     @RequiresSchemaVersion(major = 1, minor = 200)
     static final class OnLoadTrigger implements Trigger {
@@ -225,6 +379,12 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     public static @NonNull Trigger triggerFromProto(
             TriggerProto.@NonNull Trigger proto, @Nullable Fingerprint fingerprint) {
+        if (proto.hasOnVisibleTrigger()) {
+            return OnVisibleTrigger.fromProto(proto.getOnVisibleTrigger(), fingerprint);
+        }
+        if (proto.hasOnVisibleOnceTrigger()) {
+            return OnVisibleOnceTrigger.fromProto(proto.getOnVisibleOnceTrigger(), fingerprint);
+        }
         if (proto.hasOnLoadTrigger()) {
             return OnLoadTrigger.fromProto(proto.getOnLoadTrigger(), fingerprint);
         }
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt
new file mode 100644
index 0000000..d3fca75
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.wear.protolayout.ModifiersBuilders.Border
+import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.dp
+
+/**
+ * Adds a modifier to apply a border around an element.
+ *
+ * @param width The width of the border, in `DP`.
+ * @param color The color of the border.
+ */
+fun LayoutModifier.border(@Dimension(DP) width: Float, color: LayoutColor): LayoutModifier =
+    this then BaseBorderElement(width, color)
+
+internal class BaseBorderElement(@Dimension(DP) val width: Float, val color: LayoutColor) :
+    LayoutModifier.Element {
+    fun foldIn(initial: Border.Builder?): Border.Builder =
+        (initial ?: Border.Builder()).setWidth(width.dp).setColor(color.prop)
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
index 0b5531a..aee3599 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
@@ -16,15 +16,23 @@
 
 package androidx.wear.protolayout.modifiers
 
+import android.annotation.SuppressLint
+import androidx.annotation.OptIn
 import androidx.wear.protolayout.ModifiersBuilders
 import androidx.wear.protolayout.ModifiersBuilders.Background
+import androidx.wear.protolayout.ModifiersBuilders.Border
 import androidx.wear.protolayout.ModifiersBuilders.Clickable
 import androidx.wear.protolayout.ModifiersBuilders.Corner
 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
 import androidx.wear.protolayout.ModifiersBuilders.Padding
 import androidx.wear.protolayout.ModifiersBuilders.Semantics
+import androidx.wear.protolayout.TypeBuilders.BoolProp
+import androidx.wear.protolayout.TypeBuilders.FloatProp
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
 
 /** Creates a [ModifiersBuilders.Modifiers] from a [LayoutModifier]. */
+@SuppressLint("ProtoLayoutMinSchema")
+@OptIn(ProtoLayoutExperimental::class)
 fun LayoutModifier.toProtoLayoutModifiers(): ModifiersBuilders.Modifiers {
     var semantics: Semantics.Builder? = null
     var background: Background.Builder? = null
@@ -32,6 +40,9 @@
     var clickable: Clickable.Builder? = null
     var padding: Padding.Builder? = null
     var metadata: ElementMetadata.Builder? = null
+    var border: Border.Builder? = null
+    var visible: BoolProp.Builder? = null
+    var opacity: FloatProp.Builder? = null
 
     this.foldIn(Unit) { _, e ->
         when (e) {
@@ -41,6 +52,9 @@
             is BaseClickableElement -> clickable = e.foldIn(clickable)
             is BasePaddingElement -> padding = e.foldIn(padding)
             is BaseMetadataElement -> metadata = e.foldIn(metadata)
+            is BaseBorderElement -> border = e.foldIn(border)
+            is BaseVisibilityElement -> visible = e.foldIn(visible)
+            is BaseOpacityElement -> opacity = e.foldIn(opacity)
         }
     }
 
@@ -53,6 +67,9 @@
             clickable?.let { setClickable(it.build()) }
             padding?.let { setPadding(it.build()) }
             metadata?.let { setMetadata(it.build()) }
+            border?.let { setBorder(it.build()) }
+            visible?.let { setVisible(it.build()) }
+            opacity?.let { setOpacity(it.build()) }
         }
         .build()
 }
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt
new file mode 100644
index 0000000..eeafb8f9
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.annotation.FloatRange
+import androidx.wear.protolayout.TypeBuilders.FloatProp
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+
+/**
+ * Adds a modifier to specify the opacity of the element with a value from 0 to 1, where 0 means the
+ * element is completely transparent and 1 means the element is completely opaque.
+ *
+ * @param staticValue The static value for opacity. This value will be used if [dynamicValue] is
+ *   null, or if can't be resolved.
+ * @param dynamicValue The dynamic value for opacity. This can be used to change the opacity of the
+ *   element dynamically (without changing the layout definition). To create a smooth transition for
+ *   the dynamic change, you can use one of [DynamicFloat.animate] methods.
+ */
+@RequiresSchemaVersion(major = 1, minor = 400)
+fun LayoutModifier.opacity(
+    @FloatRange(from = 0.0, to = 1.0) staticValue: Float,
+    dynamicValue: DynamicFloat? = null
+): LayoutModifier = this then BaseOpacityElement(staticValue, dynamicValue)
+
+@RequiresSchemaVersion(major = 1, minor = 400)
+internal class BaseOpacityElement(val staticValue: Float, val dynamicValue: DynamicFloat? = null) :
+    LayoutModifier.Element {
+    @SuppressLint("ProtoLayoutMinSchema")
+    fun foldIn(initial: FloatProp.Builder?): FloatProp.Builder =
+        (initial ?: FloatProp.Builder(staticValue)).apply {
+            dynamicValue?.let { setDynamicValue(it) }
+        }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt
new file mode 100644
index 0000000..b7eabd7
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.wear.protolayout.TypeBuilders.BoolProp
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+
+/**
+ * Adds a modifier to specify the visibility of the element. A hidden element still consume space in
+ * the layout, but will not render any contents, nor will any of its children render any contents.
+ *
+ * Note that hidden elements won't receive input events.
+ *
+ * @param staticVisibility The static value for visibility. This value will be used if
+ *   [dynamicVisibility] is null, or if can't be resolved.
+ * @param dynamicVisibility The dynamic value for visibility. This can be used to change the
+ *   visibility of the element dynamically (without changing the layout definition).
+ */
+@RequiresSchemaVersion(major = 1, minor = 300)
+@ProtoLayoutExperimental
+fun LayoutModifier.visibility(
+    staticVisibility: Boolean,
+    dynamicVisibility: DynamicBool? = null
+): LayoutModifier = this then BaseVisibilityElement(staticVisibility, dynamicVisibility)
+
+@RequiresSchemaVersion(major = 1, minor = 300)
+internal class BaseVisibilityElement(
+    val visibility: Boolean,
+    val dynamicVisibility: DynamicBool? = null
+) : LayoutModifier.Element {
+    @SuppressLint("ProtoLayoutMinSchema")
+    fun foldIn(initial: BoolProp.Builder?): BoolProp.Builder =
+        (initial ?: BoolProp.Builder(visibility)).apply {
+            dynamicVisibility?.let { setDynamicValue(it) }
+        }
+}
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
index 3591875..c0256c3 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
@@ -74,9 +74,25 @@
                         .setProgress(DynamicBuilders.DynamicFloat.from(new AppDataKey<>(stateKey)))
                         .build();
 
-        ResourceProto.AndroidLottieResourceByResId avdProto = lottieResource.toProto();
+        ResourceProto.AndroidLottieResourceByResId lottieProto = lottieResource.toProto();
 
-        assertThat(avdProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
-        assertThat(avdProto.getProgress().getStateSource().getSourceKey()).isEqualTo(stateKey);
+        assertThat(lottieProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
+        assertThat(lottieProto.getProgress().getStateSource().getSourceKey()).isEqualTo(stateKey);
+    }
+
+    @Test
+    public void lottieAnimation_hasTrigger() {
+        ResourceBuilders.AndroidLottieResourceByResId lottieResource =
+                new ResourceBuilders.AndroidLottieResourceByResId.Builder(RESOURCE_ID)
+                        .setStartTrigger(TriggerBuilders.createOnVisibleTrigger())
+                        .build();
+
+        ResourceProto.AndroidLottieResourceByResId lottieProto = lottieResource.toProto();
+
+        assertThat(lottieProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
+        assertThat(lottieProto.hasStartTrigger()).isTrue();
+        assertThat(lottieProto.getStartTrigger().hasOnVisibleTrigger()).isTrue();
+        assertThat(lottieProto.getStartTrigger().hasOnVisibleOnceTrigger()).isFalse();
+        assertThat(lottieProto.getStartTrigger().hasOnLoadTrigger()).isFalse();
     }
 }
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
index 80f8b5e..ca402dc 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
@@ -24,6 +24,7 @@
 import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
 import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_NONE
 import androidx.wear.protolayout.expression.AppDataKey
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue
@@ -241,6 +242,26 @@
         assertThat(modifiers.metadata?.tagData).isEqualTo(METADATA_BYTE_ARRAY)
     }
 
+    @Test
+    fun border_toModifier() {
+        val modifier =
+            LayoutModifier.border(width = WIDTH_DP, color = COLOR).toProtoLayoutModifiers()
+
+        assertThat(modifier.border?.width?.value).isEqualTo(WIDTH_DP)
+        assertThat(modifier.border?.color?.argb).isEqualTo(COLOR.prop.argb)
+    }
+
+    @Test
+    fun visibility_toModifier() {
+        val modifier =
+            LayoutModifier.visibility(staticVisibility = false, dynamicVisibility = DYNAMIC_BOOL)
+                .toProtoLayoutModifiers()
+
+        assertThat(modifier.isVisible.value).isEqualTo(false)
+        assertThat(modifier.isVisible.dynamicValue?.toDynamicBoolProto())
+            .isEqualTo(DYNAMIC_BOOL.toDynamicBoolProto())
+    }
+
     companion object {
         const val STATIC_CONTENT_DESCRIPTION = "content desc"
         val DYNAMIC_CONTENT_DESCRIPTION = DynamicString.constant("dynamic content")
@@ -255,5 +276,7 @@
         const val PADDING_ALL = 5f
         const val METADATA = "metadata"
         val METADATA_BYTE_ARRAY = METADATA.toByteArray()
+        const val WIDTH_DP = 5f
+        val DYNAMIC_BOOL = DynamicBool.constant(true)
     }
 }
diff --git a/wear/wear-phone-interactions/api/1.1.0-beta01.txt b/wear/wear-phone-interactions/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..22d6f21
--- /dev/null
+++ b/wear/wear-phone-interactions/api/1.1.0-beta01.txt
@@ -0,0 +1,170 @@
+// Signature format: 4.0
+package androidx.wear.phone.interactions {
+
+  public final class PhoneTypeHelper {
+    method public static int getPhoneDeviceType(android.content.Context context);
+    field public static final androidx.wear.phone.interactions.PhoneTypeHelper.Companion Companion;
+    field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
+    field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
+    field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+    field public static final int DEVICE_TYPE_NONE = 4; // 0x4
+    field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
+  }
+
+  public static final class PhoneTypeHelper.Companion {
+    method public int getPhoneDeviceType(android.content.Context context);
+    property public static final int DEVICE_TYPE_ANDROID;
+    property public static final int DEVICE_TYPE_ERROR;
+    property public static final int DEVICE_TYPE_IOS;
+    property public static final int DEVICE_TYPE_NONE;
+    property public static final int DEVICE_TYPE_UNKNOWN;
+  }
+
+}
+
+package androidx.wear.phone.interactions.authentication {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
+    ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
+    method public String getValue();
+    property public final String value;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
+    ctor public CodeVerifier();
+    ctor public CodeVerifier(optional int byteLength);
+    ctor public CodeVerifier(String value);
+    method public String getValue();
+    property public final String value;
+  }
+
+  public final class OAuthRequest {
+    method public String getPackageName();
+    method public String getRedirectUrl();
+    method public android.net.Uri getRequestUrl();
+    property public final String packageName;
+    property public final String redirectUrl;
+    property public final android.net.Uri requestUrl;
+    field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
+    field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
+    field public static final String WEAR_REDIRECT_URL_PREFIX_CN = "https://wear.googleapis-cn.com/3p_auth/";
+  }
+
+  public static final class OAuthRequest.Builder {
+    ctor public OAuthRequest.Builder(android.content.Context context);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.wear.phone.interactions.authentication.OAuthRequest build();
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setAuthProviderUrl(android.net.Uri authProviderUrl);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setClientId(String clientId);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setCodeChallenge(androidx.wear.phone.interactions.authentication.CodeChallenge codeChallenge);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setRedirectUrl(android.net.Uri redirectUrl);
+  }
+
+  public static final class OAuthRequest.Companion {
+    property public static final String WEAR_REDIRECT_URL_PREFIX;
+    property public static final String WEAR_REDIRECT_URL_PREFIX_CN;
+  }
+
+  public final class OAuthResponse {
+    method public int getErrorCode();
+    method public android.net.Uri? getResponseUrl();
+    property public final int errorCode;
+    property public final android.net.Uri? responseUrl;
+  }
+
+  public static final class OAuthResponse.Builder {
+    ctor public OAuthResponse.Builder();
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse build();
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setErrorCode(int errorCode);
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setResponseUrl(android.net.Uri responseUrl);
+  }
+
+  public final class RemoteAuthClient implements java.lang.AutoCloseable {
+    method @UiThread public void close();
+    method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+    method protected void finalize();
+    method public kotlinx.coroutines.flow.Flow<java.lang.Integer> getAvailabilityStatus();
+    method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+    property public final kotlinx.coroutines.flow.Flow<java.lang.Integer> availabilityStatus;
+    field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
+    field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+    field public static final int NO_ERROR = -1; // 0xffffffff
+    field public static final int STATUS_AVAILABLE = 3; // 0x3
+    field public static final int STATUS_TEMPORARILY_UNAVAILABLE = 2; // 0x2
+    field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+  }
+
+  public abstract static class RemoteAuthClient.Callback {
+    ctor public RemoteAuthClient.Callback();
+    method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, int errorCode);
+    method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
+  }
+
+  public static final class RemoteAuthClient.Companion {
+    method public androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+    property public static final int ERROR_PHONE_UNAVAILABLE;
+    property public static final int ERROR_UNSUPPORTED;
+    property public static final int NO_ERROR;
+    property public static final int STATUS_AVAILABLE;
+    property public static final int STATUS_TEMPORARILY_UNAVAILABLE;
+    property public static final int STATUS_UNAVAILABLE;
+    property public static final int STATUS_UNKNOWN;
+  }
+
+  public interface RemoteAuthRequestHandler {
+    method public boolean isAuthSupported();
+    method public void sendAuthRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+  }
+
+  public abstract class RemoteAuthService extends android.app.Service {
+    ctor public RemoteAuthService();
+    method protected final android.os.IBinder onBind(android.content.Intent intent, androidx.wear.phone.interactions.authentication.RemoteAuthRequestHandler remoteAuthRequestHandler);
+    method public static final void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+    method protected boolean verifyPackageName(android.content.Context context, String? requestPackageName);
+    field public static final androidx.wear.phone.interactions.authentication.RemoteAuthService.Companion Companion;
+  }
+
+  public static final class RemoteAuthService.Companion {
+    method public void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+  }
+
+}
+
+package androidx.wear.phone.interactions.notifications {
+
+  public final class BridgingConfig {
+    method public java.util.Set<java.lang.String>? getExcludedTags();
+    method public boolean isBridgingEnabled();
+    property public final java.util.Set<java.lang.String>? excludedTags;
+    property public final boolean isBridgingEnabled;
+  }
+
+  public static final class BridgingConfig.Builder {
+    ctor public BridgingConfig.Builder(android.content.Context context, boolean isBridgingEnabled);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTag(String tag);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTags(java.util.Collection<java.lang.String> tags);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig build();
+  }
+
+  public fun interface BridgingConfigurationHandler {
+    method public void applyBridgingConfiguration(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+  }
+
+  public final class BridgingManager {
+    method public static androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+    method public void setConfig(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+    field public static final androidx.wear.phone.interactions.notifications.BridgingManager.Companion Companion;
+  }
+
+  public static final class BridgingManager.Companion {
+    method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+  }
+
+  public final class BridgingManagerService extends android.app.Service {
+    ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+    method public android.os.IBinder? onBind(android.content.Intent? intent);
+  }
+
+}
+
diff --git a/wear/wear-phone-interactions/api/current.txt b/wear/wear-phone-interactions/api/current.txt
index 4659ee8..22d6f21 100644
--- a/wear/wear-phone-interactions/api/current.txt
+++ b/wear/wear-phone-interactions/api/current.txt
@@ -7,6 +7,7 @@
     field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
     field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
     field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+    field public static final int DEVICE_TYPE_NONE = 4; // 0x4
     field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
   }
 
@@ -15,6 +16,7 @@
     property public static final int DEVICE_TYPE_ANDROID;
     property public static final int DEVICE_TYPE_ERROR;
     property public static final int DEVICE_TYPE_IOS;
+    property public static final int DEVICE_TYPE_NONE;
     property public static final int DEVICE_TYPE_UNKNOWN;
   }
 
diff --git a/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt b/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt
diff --git a/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt b/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..0798d72
--- /dev/null
+++ b/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,173 @@
+// Signature format: 4.0
+package androidx.wear.phone.interactions {
+
+  public final class PhoneTypeHelper {
+    method public static int getPhoneDeviceType(android.content.Context context);
+    field public static final androidx.wear.phone.interactions.PhoneTypeHelper.Companion Companion;
+    field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
+    field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
+    field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+    field public static final int DEVICE_TYPE_NONE = 4; // 0x4
+    field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
+  }
+
+  public static final class PhoneTypeHelper.Companion {
+    method public int getPhoneDeviceType(android.content.Context context);
+    property public static final int DEVICE_TYPE_ANDROID;
+    property public static final int DEVICE_TYPE_ERROR;
+    property public static final int DEVICE_TYPE_IOS;
+    property public static final int DEVICE_TYPE_NONE;
+    property public static final int DEVICE_TYPE_UNKNOWN;
+  }
+
+}
+
+package androidx.wear.phone.interactions.authentication {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
+    ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
+    method public String getValue();
+    property public final String value;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
+    ctor public CodeVerifier();
+    ctor public CodeVerifier(optional int byteLength);
+    ctor public CodeVerifier(String value);
+    method public String getValue();
+    property public final String value;
+  }
+
+  public final class OAuthRequest {
+    method public String getPackageName();
+    method public String getRedirectUrl();
+    method public android.net.Uri getRequestUrl();
+    property public final String packageName;
+    property public final String redirectUrl;
+    property public final android.net.Uri requestUrl;
+    field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
+    field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
+    field public static final String WEAR_REDIRECT_URL_PREFIX_CN = "https://wear.googleapis-cn.com/3p_auth/";
+  }
+
+  public static final class OAuthRequest.Builder {
+    ctor public OAuthRequest.Builder(android.content.Context context);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.wear.phone.interactions.authentication.OAuthRequest build();
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setAuthProviderUrl(android.net.Uri authProviderUrl);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setClientId(String clientId);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setCodeChallenge(androidx.wear.phone.interactions.authentication.CodeChallenge codeChallenge);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setRedirectUrl(android.net.Uri redirectUrl);
+  }
+
+  public static final class OAuthRequest.Companion {
+    property public static final String WEAR_REDIRECT_URL_PREFIX;
+    property public static final String WEAR_REDIRECT_URL_PREFIX_CN;
+  }
+
+  public final class OAuthResponse {
+    method public int getErrorCode();
+    method public android.net.Uri? getResponseUrl();
+    property @androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode public final int errorCode;
+    property public final android.net.Uri? responseUrl;
+  }
+
+  public static final class OAuthResponse.Builder {
+    ctor public OAuthResponse.Builder();
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse build();
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setErrorCode(@androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode int errorCode);
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setResponseUrl(android.net.Uri responseUrl);
+  }
+
+  public final class RemoteAuthClient implements java.lang.AutoCloseable {
+    method @UiThread public void close();
+    method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+    method protected void finalize();
+    method public kotlinx.coroutines.flow.Flow<java.lang.Integer> getAvailabilityStatus();
+    method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+    property public final kotlinx.coroutines.flow.Flow<java.lang.Integer> availabilityStatus;
+    field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
+    field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+    field public static final int NO_ERROR = -1; // 0xffffffff
+    field public static final int STATUS_AVAILABLE = 3; // 0x3
+    field public static final int STATUS_TEMPORARILY_UNAVAILABLE = 2; // 0x2
+    field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+  }
+
+  public abstract static class RemoteAuthClient.Callback {
+    ctor public RemoteAuthClient.Callback();
+    method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, @androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode int errorCode);
+    method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
+  }
+
+  public static final class RemoteAuthClient.Companion {
+    method public androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+    property public static final int ERROR_PHONE_UNAVAILABLE;
+    property public static final int ERROR_UNSUPPORTED;
+    property public static final int NO_ERROR;
+    property public static final int STATUS_AVAILABLE;
+    property public static final int STATUS_TEMPORARILY_UNAVAILABLE;
+    property public static final int STATUS_UNAVAILABLE;
+    property public static final int STATUS_UNKNOWN;
+  }
+
+  @IntDef({androidx.wear.phone.interactions.authentication.RemoteAuthClient.NO_ERROR, androidx.wear.phone.interactions.authentication.RemoteAuthClient.ERROR_UNSUPPORTED, androidx.wear.phone.interactions.authentication.RemoteAuthClient.ERROR_PHONE_UNAVAILABLE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) public static @interface RemoteAuthClient.Companion.ErrorCode {
+  }
+
+  public interface RemoteAuthRequestHandler {
+    method public boolean isAuthSupported();
+    method public void sendAuthRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+  }
+
+  public abstract class RemoteAuthService extends android.app.Service {
+    ctor public RemoteAuthService();
+    method protected final android.os.IBinder onBind(android.content.Intent intent, androidx.wear.phone.interactions.authentication.RemoteAuthRequestHandler remoteAuthRequestHandler);
+    method public static final void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+    method protected boolean verifyPackageName(android.content.Context context, String? requestPackageName);
+    field public static final androidx.wear.phone.interactions.authentication.RemoteAuthService.Companion Companion;
+  }
+
+  public static final class RemoteAuthService.Companion {
+    method public void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+  }
+
+}
+
+package androidx.wear.phone.interactions.notifications {
+
+  public final class BridgingConfig {
+    method public java.util.Set<java.lang.String>? getExcludedTags();
+    method public boolean isBridgingEnabled();
+    property public final java.util.Set<java.lang.String>? excludedTags;
+    property public final boolean isBridgingEnabled;
+  }
+
+  public static final class BridgingConfig.Builder {
+    ctor public BridgingConfig.Builder(android.content.Context context, boolean isBridgingEnabled);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTag(String tag);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTags(java.util.Collection<java.lang.String> tags);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig build();
+  }
+
+  public fun interface BridgingConfigurationHandler {
+    method public void applyBridgingConfiguration(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+  }
+
+  public final class BridgingManager {
+    method public static androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+    method public void setConfig(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+    field public static final androidx.wear.phone.interactions.notifications.BridgingManager.Companion Companion;
+  }
+
+  public static final class BridgingManager.Companion {
+    method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+  }
+
+  public final class BridgingManagerService extends android.app.Service {
+    ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+    method public android.os.IBinder? onBind(android.content.Intent? intent);
+  }
+
+}
+
diff --git a/wear/wear-phone-interactions/api/restricted_current.txt b/wear/wear-phone-interactions/api/restricted_current.txt
index 8b63c78..0798d72 100644
--- a/wear/wear-phone-interactions/api/restricted_current.txt
+++ b/wear/wear-phone-interactions/api/restricted_current.txt
@@ -7,6 +7,7 @@
     field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
     field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
     field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+    field public static final int DEVICE_TYPE_NONE = 4; // 0x4
     field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
   }
 
@@ -15,6 +16,7 @@
     property public static final int DEVICE_TYPE_ANDROID;
     property public static final int DEVICE_TYPE_ERROR;
     property public static final int DEVICE_TYPE_IOS;
+    property public static final int DEVICE_TYPE_NONE;
     property public static final int DEVICE_TYPE_UNKNOWN;
   }
 
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
index 4be01cd..676a0ca 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
@@ -34,9 +34,14 @@
                 .path(BLUETOOTH_MODE)
                 .build()
 
+        /**
+         * These values follow the values of platform constants defined in
+         * [Settings.Global.Wearable.PAIRED_DEVICE_OS_TYPE].
+         */
         internal const val UNKNOWN_MODE = 0
         internal const val ANDROID_MODE = 1
         internal const val IOS_MODE = 2
+        internal const val NONE_PAIRED_MODE = 3
 
         /** Indicates an error returned retrieving the type of phone we are paired to. */
         public const val DEVICE_TYPE_ERROR: Int = 0
@@ -50,18 +55,22 @@
         /** Indicates unknown type of phone we are paired to. */
         public const val DEVICE_TYPE_UNKNOWN: Int = 3
 
+        /** Indicates that device is not paired to phone. */
+        public const val DEVICE_TYPE_NONE: Int = 4
+
         /**
          * Returns the type of phone handset this Wear OS device has been paired with.
          *
-         * @return one of `DEVICE_TYPE_ERROR`, `DEVICE_TYPE_ANDROID`, `DEVICE_TYPE_IOS` or
-         *   `DEVICE_TYPE_UNKNOWN` indicating we had an error while determining the phone type, we
-         *   are paired to an Android phone, we are paired to an iOS phone or we could not determine
-         *   the phone type respectively.
+         * @return one of `DEVICE_TYPE_ERROR`, `DEVICE_TYPE_ANDROID`, `DEVICE_TYPE_IOS`,
+         *   `DEVICE_TYPE_UNKNOWN` or `DEVICE_TYPE_NONE` indicating we had an error while
+         *   determining the phone type, we are paired to an Android phone, we are paired to an iOS
+         *   phone, we could not determine the phone type respectively, or no phone is paired
+         *   respectively.
          */
         @DeviceFamily
         @JvmStatic
         public fun getPhoneDeviceType(context: Context): Int {
-            var bluetoothMode = UNKNOWN_MODE
+            var pairedDeviceType = UNKNOWN_MODE
             if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                 val cursor =
                     context.contentResolver.query(BLUETOOTH_MODE_URI, null, null, null, null)
@@ -69,34 +78,41 @@
                 cursor.use {
                     while (it.moveToNext()) {
                         if (BLUETOOTH_MODE == it.getString(0)) {
-                            bluetoothMode = it.getInt(1)
+                            pairedDeviceType = it.getInt(1)
                             break
                         }
                     }
                 }
+                return when (pairedDeviceType) {
+                    ANDROID_MODE -> DEVICE_TYPE_ANDROID
+                    IOS_MODE -> DEVICE_TYPE_IOS
+                    else -> DEVICE_TYPE_UNKNOWN
+                }
             } else if (
                 Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
                     context.applicationInfo.targetSdkVersion > Build.VERSION_CODES.UPSIDE_DOWN_CAKE
             ) {
                 return DEVICE_TYPE_ANDROID
-            } else {
-                bluetoothMode =
-                    Settings.Global.getInt(
-                        context.contentResolver,
-                        PAIRED_DEVICE_OS_TYPE,
-                        UNKNOWN_MODE
-                    )
             }
-            return when (bluetoothMode) {
+            pairedDeviceType =
+                Settings.Global.getInt(context.contentResolver, PAIRED_DEVICE_OS_TYPE, UNKNOWN_MODE)
+            return when (pairedDeviceType) {
                 ANDROID_MODE -> DEVICE_TYPE_ANDROID
                 IOS_MODE -> DEVICE_TYPE_IOS
+                NONE_PAIRED_MODE -> DEVICE_TYPE_NONE
                 else -> DEVICE_TYPE_UNKNOWN
             }
         }
 
         /** Annotates a value of DeviceType. */
         @Retention(AnnotationRetention.SOURCE)
-        @IntDef(DEVICE_TYPE_ERROR, DEVICE_TYPE_ANDROID, DEVICE_TYPE_IOS, DEVICE_TYPE_UNKNOWN)
+        @IntDef(
+            DEVICE_TYPE_ERROR,
+            DEVICE_TYPE_ANDROID,
+            DEVICE_TYPE_IOS,
+            DEVICE_TYPE_UNKNOWN,
+            DEVICE_TYPE_NONE
+        )
         internal annotation class DeviceFamily
     }
 }
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
index afcf1bb..57cc0e9 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
@@ -175,6 +175,18 @@
             .isEqualTo(PhoneTypeHelper.DEVICE_TYPE_IOS)
     }
 
+    @Test
+    @Config(minSdk = 35)
+    fun testGetDeviceType_returnsNone() {
+        Settings.Global.putInt(
+            contentResolver,
+            PhoneTypeHelper.PAIRED_DEVICE_OS_TYPE,
+            PhoneTypeHelper.NONE_PAIRED_MODE
+        )
+        assertThat(getPhoneDeviceType(ApplicationProvider.getApplicationContext()))
+            .isEqualTo(PhoneTypeHelper.DEVICE_TYPE_NONE)
+    }
+
     companion object {
         private fun createFakeBluetoothModeCursor(bluetoothMode: Int): Cursor {
             val cursor = MatrixCursor(arrayOf("key", "value"))
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index 23ee69f..4a0366f 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -422,8 +422,8 @@
     method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
-    method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.PrerenderOperationCallback);
-    method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
+    method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.PrerenderOperationCallback);
+    method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void removeWebMessageListener(android.webkit.WebView, String);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.MUTE_AUDIO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void setAudioMuted(android.webkit.WebView, boolean);
     method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.DEFAULT_TRAFFICSTATS_TAGGING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDefaultTrafficStatsTag(int);
@@ -478,7 +478,7 @@
     field public static final String MUTE_AUDIO = "MUTE_AUDIO";
     field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
     field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
-    field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+    field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
     field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
     field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
     field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index 23ee69f..4a0366f 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -422,8 +422,8 @@
     method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
-    method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.PrerenderOperationCallback);
-    method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
+    method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.PrerenderOperationCallback);
+    method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void removeWebMessageListener(android.webkit.WebView, String);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.MUTE_AUDIO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void setAudioMuted(android.webkit.WebView, boolean);
     method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.DEFAULT_TRAFFICSTATS_TAGGING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDefaultTrafficStatsTag(int);
@@ -478,7 +478,7 @@
     field public static final String MUTE_AUDIO = "MUTE_AUDIO";
     field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
     field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
-    field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+    field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
     field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
     field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
     field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 0fde2c0..a461177 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -22,6 +22,7 @@
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Build;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.Looper;
 import android.webkit.ValueCallback;
@@ -35,7 +36,6 @@
 import androidx.annotation.RequiresOptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.UiThread;
-import androidx.core.os.CancellationSignal;
 import androidx.webkit.internal.ApiFeature;
 import androidx.webkit.internal.ApiHelperForM;
 import androidx.webkit.internal.ApiHelperForO;
@@ -1341,8 +1341,7 @@
     }
 
     /**
-     * Starts a URL prerender request for this WebView. Can be called from any
-     * thread.
+     * Starts a URL prerender request for this WebView. Must be called from the UI thread.
      * <p>
      * This WebView will use a URL request matching algorithm during execution
      * of all variants of {@link android.webkit.WebView#loadUrl(String)} for
@@ -1363,26 +1362,26 @@
      * <p>
      * The {@link CancellationSignal} will make the best effort to cancel an
      * in-flight prerender request; however cancellation it is not guaranteed.
-     * <p>
-     * All result callbacks will be resolved on the calling thread.
      *
      * @param webView            the WebView for which we trigger the prerender request.
      * @param url                the url associated with the prerender request.
      * @param cancellationSignal used to trigger prerender cancellation.
+     * @param callbackExecutor   the executor to resolve the callback with.
      * @param callback           callbacks for reporting result back to application.
      */
     @RequiresFeature(name = WebViewFeature.PRERENDER_WITH_URL,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-    @AnyThread
+    @UiThread
     @ExperimentalUrlPrerender
-    public static void prerenderUrlAsync(
+    public static void prerenderUrl(
             @NonNull WebView webView,
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor,
             @NonNull PrerenderOperationCallback callback) {
         ApiFeature.NoFramework feature = WebViewFeatureInternal.PRERENDER_WITH_URL;
         if (feature.isSupportedByWebView()) {
-            getProvider(webView).prerenderUrlAsync(url, cancellationSignal, callback);
+            getProvider(webView).prerenderUrl(url, cancellationSignal, callbackExecutor, callback);
         } else {
             throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
@@ -1390,28 +1389,31 @@
 
     /**
      * The same as
-     * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal, PrerenderOperationCallback)},
+     * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor, PrerenderOperationCallback)},
      * but allows customizing the request by providing {@link SpeculativeLoadingParameters}.
      *
      * @param webView            the WebView for which we trigger the prerender request.
      * @param url                the url associated with the prerender request.
      * @param cancellationSignal used to trigger prerender cancellation.
+     * @param callbackExecutor   the executor to resolve the callback with.
      * @param params             parameters to customize the prerender request.
      * @param callback           callbacks for reporting result back to application.
      */
     @RequiresFeature(name = WebViewFeature.PRERENDER_WITH_URL,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-    @AnyThread
+    @UiThread
     @ExperimentalUrlPrerender
-    public static void prerenderUrlAsync(
+    public static void prerenderUrl(
             @NonNull WebView webView,
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor,
             @NonNull SpeculativeLoadingParameters params,
             @NonNull PrerenderOperationCallback callback) {
         ApiFeature.NoFramework feature = WebViewFeatureInternal.PRERENDER_WITH_URL;
         if (feature.isSupportedByWebView()) {
-            getProvider(webView).prerenderUrlAsync(url, cancellationSignal, params, callback);
+            getProvider(webView).prerenderUrl(url, cancellationSignal, callbackExecutor, params,
+                    callback);
         } else {
             throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 18747e3..2411346 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -653,11 +653,11 @@
     /**
      * Feature for {@link #isFeatureSupported(String)}.
      * This feature covers
-     * {@link androidx.webkit.WebViewCompat#prerenderUrlAsync(WebView, String,
-     * SpeculativeLoadingParameters, CancellationSignal, PrerenderOperationCallback)}}
+     * {@link androidx.webkit.WebViewCompat#prerenderUrl(WebView, String, CancellationSignal,
+     * Executor, SpeculativeLoadingParameters, PrerenderOperationCallback)}}
      */
     @WebViewCompat.ExperimentalUrlPrerender
-    public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+    public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
 
     /**
      * Feature for {@link #isFeatureSupported(String)}.
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index fd040be..df7473b 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -31,6 +31,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.webkit.OutcomeReceiverCompat;
+import androidx.webkit.PrerenderOperationCallback;
 import androidx.webkit.Profile;
 import androidx.webkit.ProfileStore;
 import androidx.webkit.ProxyConfig;
@@ -689,8 +690,9 @@
 
     /**
      * Feature for {@link WebViewFeature#isFeatureSupported(String)}.
-     * This feature covers {@link androidx.webkit.WebViewCompat#prerenderUrlAsync(WebView, String,
-     * SpeculativeLoadingParameters, CancellationSignal, PrerenderOperationCallback)}}
+     * This feature covers
+     * {@link androidx.webkit.WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
+     * SpeculativeLoadingParameters, PrerenderOperationCallback)}}
      */
     public static final ApiFeature.NoFramework PRERENDER_WITH_URL =
             new ApiFeature.NoFramework(WebViewFeature.PRERENDER_WITH_URL,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
index 0bfb95e..44b00b1 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
@@ -18,11 +18,13 @@
 
 import android.annotation.SuppressLint;
 import android.net.Uri;
+import android.os.CancellationSignal;
+import android.webkit.ValueCallback;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
-import androidx.core.os.CancellationSignal;
+import androidx.webkit.PrerenderException;
 import androidx.webkit.PrerenderOperationCallback;
 import androidx.webkit.Profile;
 import androidx.webkit.SpeculativeLoadingParameters;
@@ -194,24 +196,58 @@
 
     /**
      * Adapter method for
-     * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal,
+     * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
      * PrerenderOperationCallback)}.
      */
-    public void prerenderUrlAsync(
+    public void prerenderUrl(
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor,
             @NonNull PrerenderOperationCallback callback) {
+
+        ValueCallback<Void> activationCallback = (value) -> {
+            // value will always be null.
+            callback.onPrerenderActivated();
+        };
+        ValueCallback<Throwable> errorCallback = (throwable) -> {
+            callback.onError(new PrerenderException("Prerender operation failed", throwable));
+        };
+        mImpl.prerenderUrl(
+                url,
+                cancellationSignal,
+                callbackExecutor,
+                activationCallback,
+                errorCallback);
     }
 
     /**
      * Adapter method for
-     * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal,
+     * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
      * SpeculativeLoadingParameters, PrerenderOperationCallback)}.
      */
-    public void prerenderUrlAsync(
+    public void prerenderUrl(
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor,
             @NonNull SpeculativeLoadingParameters params,
             @NonNull PrerenderOperationCallback callback) {
+
+        InvocationHandler paramsBoundaryInterface =
+                BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
+                        new SpeculativeLoadingParametersAdapter(params));
+        ValueCallback<Void> activationCallback = (value) -> {
+            // value will always be null.
+            callback.onPrerenderActivated();
+        };
+        ValueCallback<Throwable> errorCallback = (throwable) -> {
+            callback.onError(new PrerenderException("Prerender operation failed", throwable));
+        };
+        mImpl.prerenderUrl(
+                url,
+                cancellationSignal,
+                callbackExecutor,
+                paramsBoundaryInterface,
+                activationCallback,
+                errorCallback);
     }
 }