Merge "Add `MutableStateFlowSerializer` for serializing `MutableState`" into androidx-main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 10afede..9a7562d 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -2,6 +2,7 @@
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 ktfmt_hook = ${REPO_ROOT}/frameworks/support/development/ktfmt.sh --skip-if-empty --file=${PREUPLOAD_FILES_PREFIXED}
 warn_check_api = ${REPO_ROOT}/frameworks/support/development/apilint.py -f ${PREUPLOAD_FILES}
+relnote_required_runtime = ${REPO_ROOT}/frameworks/support/development/requirerelnote.py --module /androidx/compose/runtime/  --commit ${PREUPLOAD_COMMIT_MESSAGE} --file ${PREUPLOAD_FILES}
 
 [Builtin Hooks]
 commit_msg_changeid_field = true
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/AppFunctionSchemaDefinition.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSchemaDefinition.kt
new file mode 100644
index 0000000..3469992
--- /dev/null
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSchemaDefinition.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.appfunctions
+
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
+
+/**
+ * Annotates an interface defining the schema for an app function, outlining its input, output, and
+ * behavior
+ *
+ * Example Usage:
+ * ```kotlin
+ * @AppFunctionSchemaDefinition(name = "findNotes", version = 1, category = "Notes")
+ * interface FindNotes {
+ *   suspend fun findNotes(
+ *     appFunctionContext: AppFunctionContext,
+ *     findNotesParams: FindNotesParams,
+ *   ): List<Note>
+ * }
+ * ```
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@Retention(
+    // Binary because it's used to determine the annotation values from the compiled schema library.
+    AnnotationRetention.BINARY
+)
+@Target(AnnotationTarget.CLASS)
+public annotation class AppFunctionSchemaDefinition(
+    val name: String,
+    val version: Int,
+    val category: String
+)
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/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
index 6151d45..8acc8bc 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
@@ -20,6 +20,7 @@
 import androidx.appfunctions.compiler.core.logException
 import androidx.appfunctions.compiler.processors.AppFunctionIdProcessor
 import androidx.appfunctions.compiler.processors.AppFunctionInventoryProcessor
+import androidx.appfunctions.compiler.processors.AppFunctionLegacyIndexXmlProcessor
 import com.google.devtools.ksp.processing.KSPLogger
 import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.processing.SymbolProcessor
@@ -53,8 +54,11 @@
         override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
             val idProcessor = AppFunctionIdProcessor(environment.codeGenerator)
             val inventoryProcessor = AppFunctionInventoryProcessor(environment.codeGenerator)
+            // TODO: Add compiler option to disable legacy xml generator.
+            val legacyIndexXmlProcessor =
+                AppFunctionLegacyIndexXmlProcessor(environment.codeGenerator)
             return AppFunctionCompiler(
-                listOf(idProcessor, inventoryProcessor),
+                listOf(idProcessor, inventoryProcessor, legacyIndexXmlProcessor),
                 environment.logger,
             )
         }
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionSymbolResolver.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionSymbolResolver.kt
index 2bac5ac..a7d1d5c 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionSymbolResolver.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionSymbolResolver.kt
@@ -20,6 +20,7 @@
 import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionAnnotation
 import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSFile
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
 
 /** The helper class to resolve AppFunction related symbols. */
@@ -113,5 +114,8 @@
             val methodName = functionDeclaration.simpleName.asString()
             return "${packageName}.${className}#${methodName}"
         }
+
+        /** Returns the file containing the class declaration and app functions. */
+        fun getSourceFile(): KSFile? = classDeclaration.containingFile
     }
 }
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
index fe0d3fa..6f90f16 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
@@ -28,6 +28,14 @@
     // Annotation classes
     object AppFunctionAnnotation {
         val CLASS_NAME = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunction")
+        const val PROPERTY_IS_ENABLED = "isEnabled"
+    }
+
+    object AppFunctionSchemaDefinitionAnnotation {
+        val CLASS_NAME = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionSchemaDefinition")
+        const val PROPERTY_CATEGORY = "category"
+        const val PROPERTY_NAME = "name"
+        const val PROPERTY_VERSION = "version"
     }
 
     // Classes
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
index 1fa4707..f723a5cb 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
@@ -16,8 +16,12 @@
 
 package androidx.appfunctions.compiler.core
 
+import com.google.devtools.ksp.symbol.KSAnnotation
+import com.google.devtools.ksp.symbol.KSName
 import com.google.devtools.ksp.symbol.KSTypeReference
 import com.squareup.kotlinpoet.ClassName
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
 
 /**
  * Checks if the type reference is of the given type.
@@ -27,12 +31,45 @@
  * @throws ProcessingException If unable to resolve the type.
  */
 fun KSTypeReference.isOfType(type: ClassName): Boolean {
-    val ksType = this.resolve()
     val typeName =
-        ksType.declaration.qualifiedName
+        resolveTypeName()
             ?: throw ProcessingException(
                 "Unable to resolve the type to check if it is of type [${type}]",
                 this
             )
     return typeName.asString() == type.canonicalName
 }
+
+/**
+ * Finds and returns an annotation of [annotationClass] type.
+ *
+ * @param annotationClass the annotation class to find
+ */
+fun Sequence<KSAnnotation>.findAnnotation(annotationClass: ClassName): KSAnnotation? =
+    this.singleOrNull() {
+        val shortName = it.shortName.getShortName()
+        if (shortName != annotationClass.simpleName) {
+            false
+        } else {
+            val typeName =
+                it.annotationType.resolveTypeName()
+                    ?: throw ProcessingException(
+                        "Unable to resolve type for [$shortName]",
+                        it.annotationType
+                    )
+            typeName.asString() == annotationClass.canonicalName
+        }
+    }
+
+private fun KSTypeReference.resolveTypeName(): KSName? = resolve().declaration.qualifiedName
+
+/** Returns the value of the annotation property if found. */
+fun <T : Any> KSAnnotation.requirePropertyValueOfType(
+    propertyName: String,
+    expectedType: KClass<T>,
+): T {
+    val propertyValue =
+        this.arguments.singleOrNull { it.name?.asString() == propertyName }?.value
+            ?: throw ProcessingException("Unable to find property with name: $propertyName", this)
+    return expectedType.cast(propertyValue)
+}
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionLegacyIndexXmlProcessor.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionLegacyIndexXmlProcessor.kt
new file mode 100644
index 0000000..d41a4d3e
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionLegacyIndexXmlProcessor.kt
@@ -0,0 +1,250 @@
+/*
+ * 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.appfunctions.compiler.processors
+
+import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
+import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver.AnnotatedAppFunctions
+import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionAnnotation
+import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionSchemaDefinitionAnnotation
+import androidx.appfunctions.compiler.core.ProcessingException
+import androidx.appfunctions.compiler.core.findAnnotation
+import androidx.appfunctions.compiler.core.requirePropertyValueOfType
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.squareup.kotlinpoet.ClassName
+import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.TransformerFactory
+import javax.xml.transform.dom.DOMSource
+import javax.xml.transform.stream.StreamResult
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+
+/**
+ * Generates AppFunction's index xml file for the legacy AppSearch indexer to index.
+ *
+ * The generator would write an XML file as `/assets/app_functions.xml`. The file would be packaged
+ * into the APK's asset when assembled. So that the AppSearch indexer can look up the asset and
+ * inject metadata into platform AppSearch database accordingly.
+ *
+ * The new indexer will index additional properties based on the schema defined in SDK instead of
+ * the pre-defined one in AppSearch.
+ */
+class AppFunctionLegacyIndexXmlProcessor(
+    private val codeGenerator: CodeGenerator,
+) : SymbolProcessor {
+
+    override fun process(resolver: Resolver): List<KSAnnotated> {
+        generateLegacyIndexXml(AppFunctionSymbolResolver(resolver).resolveAnnotatedAppFunctions())
+        return emptyList()
+    }
+
+    /**
+     * Generates AppFunction's legacy index xml files for v1 indexer in App Search.
+     *
+     * @param appFunctionsByClass a collection of functions annotated with @AppFunction grouped by
+     *   their enclosing classes.
+     */
+    private fun generateLegacyIndexXml(
+        appFunctionsByClass: List<AnnotatedAppFunctions>,
+    ) {
+        if (appFunctionsByClass.isEmpty()) {
+            return
+        }
+        val xmlDetails = appFunctionsByClass.flatMap(::getAppFunctionXmlDetail)
+        writeXmlFile(xmlDetails, appFunctionsByClass)
+    }
+
+    private fun getAppFunctionXmlDetail(
+        appFunctionsByClass: AnnotatedAppFunctions
+    ): List<AppFunctionXmlDetails> {
+
+        return appFunctionsByClass.appFunctionDeclarations.map {
+            val appFunctionAnnotation =
+                it.annotations.findAnnotation(AppFunctionAnnotation.CLASS_NAME)
+                    ?: throw ProcessingException("Function not annotated with @AppFunction.", it)
+            val enabled =
+                appFunctionAnnotation.requirePropertyValueOfType(
+                    AppFunctionAnnotation.PROPERTY_IS_ENABLED,
+                    Boolean::class,
+                )
+
+            val schemaDetail = getAppFunctionSchemaDetail(it)
+
+            AppFunctionXmlDetails(
+                appFunctionsByClass.getAppFunctionIdentifier(it),
+                enabled,
+                schemaDetail,
+            )
+        }
+    }
+
+    private fun getAppFunctionSchemaDetail(
+        function: KSFunctionDeclaration
+    ): AppFunctionSchemaDetail? {
+        val rootInterfaceWithAppFunctionSchemaDefinition =
+            findRootInterfaceWithAnnotation(
+                function,
+                AppFunctionSchemaDefinitionAnnotation.CLASS_NAME
+            ) ?: return null
+
+        val schemaFunctionAnnotation =
+            rootInterfaceWithAppFunctionSchemaDefinition.annotations.findAnnotation(
+                AppFunctionSchemaDefinitionAnnotation.CLASS_NAME
+            ) ?: return null
+        val schemaCategory =
+            schemaFunctionAnnotation.requirePropertyValueOfType(
+                AppFunctionSchemaDefinitionAnnotation.PROPERTY_CATEGORY,
+                String::class,
+            )
+        val schemaName =
+            schemaFunctionAnnotation.requirePropertyValueOfType(
+                AppFunctionSchemaDefinitionAnnotation.PROPERTY_NAME,
+                String::class,
+            )
+        val schemaVersion =
+            schemaFunctionAnnotation.requirePropertyValueOfType(
+                AppFunctionSchemaDefinitionAnnotation.PROPERTY_VERSION,
+                Int::class,
+            )
+        return AppFunctionSchemaDetail(schemaCategory, schemaName, schemaVersion)
+    }
+
+    private fun findRootInterfaceWithAnnotation(
+        function: KSFunctionDeclaration,
+        annotationName: ClassName
+    ): KSClassDeclaration? {
+        val parentDeclaration = function.parentDeclaration as? KSClassDeclaration ?: return null
+
+        // Check if the enclosing class has the @AppFunctionSchemaDefinition
+        val annotation = parentDeclaration.annotations.findAnnotation(annotationName)
+        if (annotation != null) {
+            return parentDeclaration
+        }
+
+        val superClassFunction = (function.findOverridee() as? KSFunctionDeclaration) ?: return null
+        return findRootInterfaceWithAnnotation(superClassFunction, annotationName)
+    }
+
+    private fun writeXmlFile(
+        xmlDetailsList: List<AppFunctionXmlDetails>,
+        appFunctionsByClass: List<AnnotatedAppFunctions>,
+    ) {
+        val xmlDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+        val xmlDocument = xmlDocumentBuilder.newDocument().apply { xmlStandalone = true }
+
+        val appFunctionsElement = xmlDocument.createElement(XmlElement.APP_FUNCTIONS_ELEMENTS_TAG)
+        xmlDocument.appendChild(appFunctionsElement)
+
+        for (xmlDetails in xmlDetailsList) {
+            appFunctionsElement.appendChild(xmlDocument.createAppFunctionElement(xmlDetails))
+        }
+
+        val transformer =
+            TransformerFactory.newInstance().newTransformer().apply {
+                setOutputProperty(OutputKeys.INDENT, "yes")
+                setOutputProperty(OutputKeys.ENCODING, "UTF-8")
+                setOutputProperty(OutputKeys.VERSION, "1.0")
+                setOutputProperty(OutputKeys.STANDALONE, "yes")
+            }
+
+        codeGenerator
+            .createNewFile(
+                Dependencies(
+                    aggregating = true,
+                    *appFunctionsByClass.mapNotNull { it.getSourceFile() }.toTypedArray()
+                ),
+                XML_PACKAGE_NAME,
+                XML_FILE_NAME,
+                XML_EXTENSION
+            )
+            .use { stream -> transformer.transform(DOMSource(xmlDocument), StreamResult(stream)) }
+    }
+
+    private fun Document.createAppFunctionElement(xmlDetails: AppFunctionXmlDetails): Element =
+        createElement(XmlElement.APP_FUNCTION_ITEM_TAG).apply {
+            appendChild(
+                createElementWithTextNode(XmlElement.APP_FUNCTION_ID_TAG, xmlDetails.functionId)
+            )
+
+            val schemaDetail = xmlDetails.schemaDetail
+            if (schemaDetail != null) {
+                appendChild(
+                    createElementWithTextNode(
+                        XmlElement.APP_FUNCTION_SCHEMA_CATEGORY_TAG,
+                        schemaDetail.schemaCategory,
+                    )
+                )
+                appendChild(
+                    createElementWithTextNode(
+                        XmlElement.APP_FUNCTION_SCHEMA_NAME_TAG,
+                        schemaDetail.schemaName,
+                    )
+                )
+                appendChild(
+                    createElementWithTextNode(
+                        XmlElement.APP_FUNCTION_SCHEMA_VERSION_TAG,
+                        schemaDetail.schemaVersion.toString(),
+                    )
+                )
+            }
+            appendChild(
+                createElementWithTextNode(
+                    XmlElement.APP_FUNCTION_ENABLE_BY_DEFAULT_TAG,
+                    xmlDetails.enabled.toString(),
+                )
+            )
+        }
+
+    private fun Document.createElementWithTextNode(elementName: String, text: String): Element =
+        createElement(elementName).apply { appendChild(createTextNode(text)) }
+
+    /** Details of an app function that are needed to generate its XML file. */
+    private data class AppFunctionXmlDetails(
+        val functionId: String,
+        val enabled: Boolean,
+        val schemaDetail: AppFunctionSchemaDetail?,
+    )
+
+    /** Details of an schema function that are needed to generate its XML file. */
+    private data class AppFunctionSchemaDetail(
+        val schemaCategory: String,
+        val schemaName: String,
+        val schemaVersion: Int,
+    )
+
+    private companion object {
+        private const val XML_PACKAGE_NAME = "assets"
+        private const val XML_FILE_NAME = "app_functions"
+        private const val XML_EXTENSION = "xml"
+
+        private object XmlElement {
+            const val APP_FUNCTIONS_ELEMENTS_TAG = "appfunctions"
+            const val APP_FUNCTION_ITEM_TAG = "appfunction"
+            const val APP_FUNCTION_ID_TAG = "function_id"
+            const val APP_FUNCTION_SCHEMA_CATEGORY_TAG = "schema_category"
+            const val APP_FUNCTION_SCHEMA_NAME_TAG = "schema_name"
+            const val APP_FUNCTION_SCHEMA_VERSION_TAG = "schema_version"
+            const val APP_FUNCTION_ENABLE_BY_DEFAULT_TAG = "enabled_by_default"
+        }
+    }
+}
diff --git a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
index 8860052..39b1ebb 100644
--- a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
+++ b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
@@ -46,9 +46,9 @@
     fun testSimpleFunction_genAppFunctionIds_success() {
         val report = compilationTestHelper.compileAll(sourceFileNames = listOf("SimpleFunction.KT"))
 
-        compilationTestHelper.assertSuccessWithContent(
+        compilationTestHelper.assertSuccessWithSourceContent(
             report = report,
-            expectGeneratedFileName = "SimpleFunctionIds.kt",
+            expectGeneratedSourceFileName = "SimpleFunctionIds.kt",
             goldenFileName = "SimpleFunctionIds.KT"
         )
     }
@@ -85,10 +85,25 @@
     fun testSimpleFunction_genAppFunctionInventoryImpl_success() {
         val report = compilationTestHelper.compileAll(sourceFileNames = listOf("SimpleFunction.KT"))
 
-        compilationTestHelper.assertSuccessWithContent(
+        compilationTestHelper.assertSuccessWithSourceContent(
             report = report,
-            expectGeneratedFileName = "SimpleFunction_AppFunctionInventory_Impl.kt",
+            expectGeneratedSourceFileName = "SimpleFunction_AppFunctionInventory_Impl.kt",
             goldenFileName = "$%s".format("SimpleFunction_AppFunctionInventory_Impl.KT")
         )
     }
+
+    // TODO: Add more tests for legacy index processor.
+    @Test
+    fun testSampleNoParamImp_genLegacyIndexXmlFile_success() {
+        val report =
+            compilationTestHelper.compileAll(
+                sourceFileNames = listOf("FakeNoArgImpl.KT", "FakeSchemas.KT")
+            )
+
+        compilationTestHelper.assertSuccessWithResourceContent(
+            report = report,
+            expectGeneratedResourceFileName = "app_functions.xml",
+            goldenFileName = "fakeNoArgImpl_app_function.xml"
+        )
+    }
 }
diff --git a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/testings/CompilationTestHelper.kt b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/testings/CompilationTestHelper.kt
index 4e6253d..cbb3c4b 100644
--- a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/testings/CompilationTestHelper.kt
+++ b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/testings/CompilationTestHelper.kt
@@ -17,6 +17,7 @@
 package androidx.appfunctions.compiler.testings
 
 import androidx.room.compiler.processing.util.DiagnosticMessage
+import androidx.room.compiler.processing.util.Resource
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
 import androidx.room.compiler.processing.util.compiler.TestCompilationResult
@@ -99,28 +100,20 @@
     }
 
     /**
-     * Asserts that the compilation succeeds and contains [expectGeneratedFileName] in generated
-     * sources that is identical to the content of [goldenFileName].
+     * Asserts that the compilation succeeds and contains [expectGeneratedSourceFileName] in
+     * generated sources that is identical to the content of [goldenFileName].
      */
-    fun assertSuccessWithContent(
+    fun assertSuccessWithSourceContent(
         report: CompilationReport,
-        expectGeneratedFileName: String,
+        expectGeneratedSourceFileName: String,
         goldenFileName: String,
     ) {
-        Truth.assertWithMessage(
-                """
-                Compile failed with error:
-                ${report.printDiagnostics(Diagnostic.Kind.ERROR)}
-            """
-                    .trimIndent()
-            )
-            .that(report.isSuccess)
-            .isTrue()
+        assertCompilationSuccess(report)
 
         val goldenFile = getGoldenFile(goldenFileName)
         val generatedSourceFile =
             report.generatedSourceFiles.single { sourceFile ->
-                sourceFile.source.relativePath.contains(expectGeneratedFileName)
+                sourceFile.source.relativePath.contains(expectGeneratedSourceFileName)
             }
         Truth.assertWithMessage(
                 """
@@ -136,6 +129,48 @@
             .isEqualTo(goldenFile.readText())
     }
 
+    /**
+     * Asserts that the compilation succeeds and contains [expectGeneratedResourceFileName] in
+     * generated resources that is identical to the content of [goldenFileName].
+     */
+    fun assertSuccessWithResourceContent(
+        report: CompilationReport,
+        expectGeneratedResourceFileName: String,
+        goldenFileName: String,
+    ) {
+        assertCompilationSuccess(report)
+
+        val goldenFile = getGoldenFile(goldenFileName)
+        val generatedResourceFile =
+            report.generatedResourceFiles.single { resourceFile ->
+                resourceFile.resource.relativePath.contains(expectGeneratedResourceFileName)
+            }
+        Truth.assertWithMessage(
+                """
+              Content of generated file [${generatedResourceFile.resource.relativePath}] does not match
+              the content of golden file [${goldenFile.path}].
+
+              To update the golden file,
+              run `cp ${generatedResourceFile.resourceFilePath} ${goldenFile.absolutePath}`
+            """
+                    .trimIndent()
+            )
+            .that(generatedResourceFile.resource.getContents())
+            .isEqualTo(goldenFile.readText())
+    }
+
+    private fun assertCompilationSuccess(report: CompilationReport) {
+        Truth.assertWithMessage(
+                """
+                    Compile failed with error:
+                    ${report.printDiagnostics(Diagnostic.Kind.ERROR)}
+                """
+                    .trimIndent()
+            )
+            .that(report.isSuccess)
+            .isTrue()
+    }
+
     fun assertErrorWithMessage(report: CompilationReport, expectedErrorMessage: String) {
         Truth.assertWithMessage("Compile succeed").that(report.isSuccess).isFalse()
 
@@ -196,6 +231,8 @@
         val generatedSourceFiles: List<GeneratedSourceFile>,
         /** A map of diagnostics results. */
         val diagnostics: Map<Diagnostic.Kind, List<DiagnosticMessage>>,
+        /** A list of generated source files. */
+        val generatedResourceFiles: List<GeneratedResourceFile>,
     ) {
         /** Print the diagnostics result of type [kind]. */
         fun printDiagnostics(kind: Diagnostic.Kind): String {
@@ -216,7 +253,11 @@
                         result.generatedSources.map { source ->
                             GeneratedSourceFile.create(source, outputDir)
                         },
-                    diagnostics = result.diagnostics
+                    diagnostics = result.diagnostics,
+                    generatedResourceFiles =
+                        result.generatedResources.map { resource ->
+                            GeneratedResourceFile.create(resource, outputDir)
+                        }
                 )
             }
         }
@@ -236,4 +277,22 @@
             }
         }
     }
+
+    /** A wrapper class contains [Resource] with its file path. */
+    data class GeneratedResourceFile(val resource: Resource, val resourceFilePath: Path) {
+        companion object {
+            internal fun create(resource: Resource, outputDir: Path): GeneratedResourceFile {
+                val filePath =
+                    outputDir.resolve(resource.relativePath).apply {
+                        parent?.createDirectories()
+                        createFile()
+                        writeText(resource.getContents())
+                    }
+                return GeneratedResourceFile(resource, filePath)
+            }
+        }
+    }
 }
+
+private fun Resource.getContents(): String =
+    openInputStream().bufferedReader().use { it.readText() }
diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeNoArgImpl.KT b/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeNoArgImpl.KT
new file mode 100644
index 0000000..4b09194
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeNoArgImpl.KT
@@ -0,0 +1,8 @@
+package com.testdata
+
+import androidx.appfunctions.AppFunction
+import androidx.appfunctions.AppFunctionContext
+
+class FakeNoArgImpl : FakeNoArg {
+    @AppFunction override fun noArg(appFunctionContext: AppFunctionContext) {}
+}
diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeSchemas.KT b/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeSchemas.KT
new file mode 100644
index 0000000..a59c737
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/test/test-data/input/FakeSchemas.KT
@@ -0,0 +1,11 @@
+package com.testdata
+
+import androidx.appfunctions.AppFunctionContext
+import androidx.appfunctions.AppFunctionSchemaDefinition
+
+private const val FAKE_CATEGORY = "fake_schema_category"
+
+@AppFunctionSchemaDefinition(name = "noArg", version = 1, category = FAKE_CATEGORY)
+interface FakeNoArg {
+    fun noArg(appFunctionContext: AppFunctionContext)
+}
diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/output/fakeNoArgImpl_app_function.xml b/appfunctions/appfunctions-compiler/src/test/test-data/output/fakeNoArgImpl_app_function.xml
new file mode 100644
index 0000000..229bae7
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/test/test-data/output/fakeNoArgImpl_app_function.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<appfunctions>
+    <appfunction>
+        <function_id>com.testdata.FakeNoArgImpl#noArg</function_id>
+        <schema_category>fake_schema_category</schema_category>
+        <schema_name>noArg</schema_name>
+        <schema_version>1</schema_version>
+        <enabled_by_default>true</enabled_by_default>
+    </appfunction>
+</appfunctions>
diff --git a/appfunctions/appfunctions-runtime/build.gradle b/appfunctions/appfunctions-runtime/build.gradle
index a95d75a..146190f 100644
--- a/appfunctions/appfunctions-runtime/build.gradle
+++ b/appfunctions/appfunctions-runtime/build.gradle
@@ -36,8 +36,15 @@
     implementation("androidx.annotation:annotation:1.9.0-rc01")
     implementation project(":appfunctions:appfunctions-common")
 
+    // Test dependencies
     testImplementation(libs.junit)
     testImplementation(libs.truth)
+
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.kotlinCoroutinesTest)
 }
 
 android {
diff --git a/appfunctions/appfunctions-runtime/src/androidTest/java/androidx/appfunctions/internal/AggregateAppFunctionInvokerTest.kt b/appfunctions/appfunctions-runtime/src/androidTest/java/androidx/appfunctions/internal/AggregateAppFunctionInvokerTest.kt
new file mode 100644
index 0000000..1d63cd0
--- /dev/null
+++ b/appfunctions/appfunctions-runtime/src/androidTest/java/androidx/appfunctions/internal/AggregateAppFunctionInvokerTest.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.appfunctions.internal
+
+import android.content.Context
+import android.content.pm.SigningInfo
+import androidx.appfunctions.AppFunctionContext
+import androidx.appfunctions.AppFunctionFunctionNotFoundException
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Test
+
+class AggregateAppFunctionInvokerTest {
+
+    @Test
+    fun testEmptyAggregateInvoker() {
+        val aggregateInvoker =
+            object : AggregateAppFunctionInvoker() {
+                override val invokers: List<AppFunctionInvoker> = emptyList()
+            }
+
+        assertThat(aggregateInvoker.supportedFunctionIds).isEmpty()
+        Assert.assertThrows(AppFunctionFunctionNotFoundException::class.java) {
+            runBlocking {
+                aggregateInvoker.unsafeInvoke(
+                    FakeAppFunctionContext,
+                    "androidx.apfunctions.internal#test1",
+                    mapOf()
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testAggregateInvoker_nonExistFunction() {
+        val aggregateInvoker =
+            object : AggregateAppFunctionInvoker() {
+                override val invokers: List<AppFunctionInvoker> =
+                    listOf(
+                        Invoker1(),
+                        Invoker2(),
+                    )
+            }
+
+        assertThat(aggregateInvoker.supportedFunctionIds).hasSize(2)
+        assertThat(aggregateInvoker.supportedFunctionIds)
+            .containsNoneIn(listOf("androidx.appfunctions.internal#test0"))
+        Assert.assertThrows(AppFunctionFunctionNotFoundException::class.java) {
+            runBlocking {
+                aggregateInvoker.unsafeInvoke(
+                    FakeAppFunctionContext,
+                    "androidx.apfunctions.internal#test0",
+                    mapOf()
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testAggregateInvoker_existFunctions() {
+        val aggregateInvoker =
+            object : AggregateAppFunctionInvoker() {
+                override val invokers: List<AppFunctionInvoker> =
+                    listOf(
+                        Invoker1(),
+                        Invoker2(),
+                    )
+            }
+
+        val invokeTest1Result = runBlocking {
+            aggregateInvoker.unsafeInvoke(
+                FakeAppFunctionContext,
+                "androidx.appfunctions.internal#test1",
+                mapOf()
+            )
+        }
+        val invokeTest2Result = runBlocking {
+            aggregateInvoker.unsafeInvoke(
+                FakeAppFunctionContext,
+                "androidx.appfunctions.internal#test2",
+                mapOf()
+            )
+        }
+
+        assertThat(aggregateInvoker.supportedFunctionIds)
+            .containsExactly(
+                "androidx.appfunctions.internal#test1",
+                "androidx.appfunctions.internal#test2"
+            )
+        assertThat(invokeTest1Result).isEqualTo("Invoker1#test1")
+        assertThat(invokeTest2Result).isEqualTo("Invoker2#test2")
+    }
+
+    private class Invoker1 : AppFunctionInvoker {
+        override val supportedFunctionIds: Set<String> =
+            setOf("androidx.appfunctions.internal#test1")
+
+        override suspend fun unsafeInvoke(
+            appFunctionContext: AppFunctionContext,
+            functionIdentifier: String,
+            parameters: Map<String, Any?>
+        ): Any? {
+            return when (functionIdentifier) {
+                "androidx.appfunctions.internal#test1" -> "Invoker1#test1"
+                else -> throw IllegalArgumentException()
+            }
+        }
+    }
+
+    private class Invoker2 : AppFunctionInvoker {
+        override val supportedFunctionIds: Set<String> =
+            setOf("androidx.appfunctions.internal#test2")
+
+        override suspend fun unsafeInvoke(
+            appFunctionContext: AppFunctionContext,
+            functionIdentifier: String,
+            parameters: Map<String, Any?>
+        ): Any? {
+            return when (functionIdentifier) {
+                "androidx.appfunctions.internal#test2" -> "Invoker2#test2"
+                else -> throw IllegalArgumentException()
+            }
+        }
+    }
+
+    private object FakeAppFunctionContext : AppFunctionContext {
+        override val context: Context
+            get() = throw RuntimeException("Stub!")
+
+        override val callingPackageName: String
+            get() = throw RuntimeException("Stub!")
+
+        override val callingPackageSigningInfo: SigningInfo
+            get() = throw RuntimeException("Stub!")
+    }
+}
diff --git a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInvoker.kt b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInvoker.kt
new file mode 100644
index 0000000..df6be5a
--- /dev/null
+++ b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInvoker.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.appfunctions.internal
+
+import androidx.annotation.RestrictTo
+import androidx.appfunctions.AppFunctionContext
+import androidx.appfunctions.AppFunctionFunctionNotFoundException
+
+/**
+ * An [AppFunctionInvoker] that will delegate [unsafeInvoke] to the implementation that supports the
+ * given function call request.
+ *
+ * AppFunction compiler will automatically generate the implementation of this class.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class AggregateAppFunctionInvoker : AppFunctionInvoker {
+
+    /** The list of [AppFunctionInvoker] instances that contribute to this aggregate. */
+    public abstract val invokers: List<AppFunctionInvoker>
+
+    final override val supportedFunctionIds: Set<String> by lazy {
+        // Empty collection can't be reduced
+        if (invokers.isEmpty()) return@lazy emptySet<String>()
+        invokers.map(AppFunctionInvoker::supportedFunctionIds).reduce { acc, ids -> acc + ids }
+    }
+
+    final override suspend fun unsafeInvoke(
+        appFunctionContext: AppFunctionContext,
+        functionIdentifier: String,
+        parameters: Map<String, Any?>
+    ): Any? {
+        for (invoker in invokers) {
+            if (invoker.supportedFunctionIds.contains(functionIdentifier)) {
+                return invoker.unsafeInvoke(appFunctionContext, functionIdentifier, parameters)
+            }
+        }
+        throw AppFunctionFunctionNotFoundException("Unable to find $functionIdentifier")
+    }
+}
diff --git a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
index 59c4bca..4567641 100644
--- a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
+++ b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
@@ -50,11 +50,10 @@
     /**
      * Invokes an AppFunction identified by [functionIdentifier], with [parameters].
      *
-     * @throws [androidx.appfunctions.AppFunctionException] with error code
-     *   [androidx.appfunctions.AppFunctionException.ERROR_FUNCTION_NOT_FOUND] if called with
-     *   invalid function identifier or code
-     *   [androidx.appfunctions.AppFunctionException.ERROR_INVALID_ARGUMENT] if called with invalid
-     *   parameters.
+     * @throws [androidx.appfunctions.AppFunctionFunctionNotFoundException] if [functionIdentifier]
+     *   does not exist.
+     * @throws [androidx.appfunctions.AppFunctionInvalidArgumentException] if [parameters] is
+     *   invalid.
      */
     public suspend fun unsafeInvoke(
         appFunctionContext: AppFunctionContext,
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 898e680..724fb0b 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -110,6 +110,7 @@
 import org.junit.rules.TemporaryFolder;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -2300,6 +2301,42 @@
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
+    public void testWriteAfterCommit_notAllowed() throws Exception {
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()),
+                /*initStatsBuilder=*/ null,
+                /*visibilityChecker=*/ null,
+                new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
+                ALWAYS_OPTIMIZE);
+        byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
+        byte[] digest = calculateDigest(data);
+        AppSearchBlobHandle handle = AppSearchBlobHandle.createWithSha256(
+                digest, "package", "db1", "ns");
+        // Open a pfd for write, write the blob data without close the pfd.
+        ParcelFileDescriptor writePfd = mAppSearchImpl.openWriteBlob("package", "db1", handle);
+        try (FileOutputStream outputStream = new FileOutputStream(writePfd.getFileDescriptor())) {
+            outputStream.write(data);
+            outputStream.flush();
+        }
+
+        // Commit the blob.
+        mAppSearchImpl.commitBlob("package", "db1", handle);
+
+        // Keep writing to the pfd for write.
+        assertThrows(IOException.class,
+                () -> {
+                try (FileOutputStream outputStream =
+                         new FileOutputStream(writePfd.getFileDescriptor())) {
+                    outputStream.write(data);
+                    outputStream.flush();
+                }
+            });
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
     public void testRemovePendingBlob() throws Exception {
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -2470,6 +2507,66 @@
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
+    public void testOpenMultipleBlobForWrite() throws Exception {
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()),
+                /*initStatsBuilder=*/ null,
+                /*visibilityChecker=*/ null,
+                new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
+                ALWAYS_OPTIMIZE);
+        byte[] data = generateRandomBytes(20); // 20 Bytes
+        byte[] digest = calculateDigest(data);
+        AppSearchBlobHandle handle = AppSearchBlobHandle.createWithSha256(
+                digest, "package", "db1", "ns");
+
+        // only allow open 1 fd for writing.
+        try (ParcelFileDescriptor writePfd1 =
+                     mAppSearchImpl.openWriteBlob("package", "db1", handle);
+                ParcelFileDescriptor writePfd2 =
+                        mAppSearchImpl.openWriteBlob("package", "db1", handle)) {
+            assertThat(writePfd1).isEqualTo(writePfd2);
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
+    public void testOpenMultipleBlobForRead() throws Exception {
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()),
+                /*initStatsBuilder=*/ null,
+                /*visibilityChecker=*/ null,
+                new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
+                ALWAYS_OPTIMIZE);
+        byte[] data = generateRandomBytes(20); // 20 Bytes
+        byte[] digest = calculateDigest(data);
+        AppSearchBlobHandle handle = AppSearchBlobHandle.createWithSha256(
+                digest, "package", "db1", "ns");
+
+        // write a blob first.
+        try (ParcelFileDescriptor writePfd = mAppSearchImpl.openWriteBlob("package", "db1", handle);
+                OutputStream outputStream = new ParcelFileDescriptor
+                        .AutoCloseOutputStream(writePfd)) {
+            outputStream.write(data);
+            outputStream.flush();
+        }
+        // commit the change and read the blob.
+        mAppSearchImpl.commitBlob("package", "db1", handle);
+
+        // allow open multiple fd for reading.
+        try (ParcelFileDescriptor readPfd1 =
+                     mAppSearchImpl.openReadBlob("package", "db1", handle);
+                ParcelFileDescriptor readPfd2 =
+                        mAppSearchImpl.openReadBlob("package", "db1", handle)) {
+            assertThat(readPfd1).isNotEqualTo(readPfd2);
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
     public void testOptimizeBlob() throws Exception {
         // Create a new AppSearchImpl with lower orphan blob time to live.
         mAppSearchImpl.close();
@@ -6110,6 +6207,19 @@
                         + "descriptors. Some file descriptors must be closed to open additional "
                         + "ones.");
 
+        // Open new fd for write will also fail since read and write share the same limit.
+        byte[] data2 = generateRandomBytes(20 * 1024); // 20 KiB
+        byte[] digest2 = calculateDigest(data2);
+        AppSearchBlobHandle handle2 = AppSearchBlobHandle.createWithSha256(
+                digest2, mContext.getPackageName(), "db1", "ns");
+        e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.openWriteBlob(mContext.getPackageName(), "db1", handle2));
+        assertThat(e.getResultCode()).isEqualTo(RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"" + mContext.getPackageName() + "\" exceeded limit of 2 opened file "
+                        + "descriptors. Some file descriptors must be closed to open additional "
+                        + "ones.");
+
         // Close 1st fd and open 3rd fd will success
         reader1.close();
         ParcelFileDescriptor reader3 =
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 1dfb53a..4dbbaf5 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -31,6 +31,7 @@
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
@@ -248,6 +249,7 @@
     @GuardedBy("mReadWriteLock")
     private int mOptimizeIntervalCountLocked = 0;
 
+    @ExperimentalAppSearchApi
     private final @Nullable RevocableFileDescriptorStore mRevocableFileDescriptorStore;
 
     /** Whether this instance has been closed, and therefore unusable. */
@@ -270,6 +272,7 @@
      *                          access to aa specific schema. Pass null will lost that ability and
      *                          global querier could only get their own data.
      */
+    @OptIn(markerClass = ExperimentalAppSearchApi.class)
     public static @NonNull AppSearchImpl create(
             @NonNull File icingDir,
             @NonNull AppSearchConfig config,
@@ -285,6 +288,7 @@
     /**
      * @param initStatsBuilder collects stats for initialization if provided.
      */
+    @OptIn(markerClass = ExperimentalAppSearchApi.class)
     private AppSearchImpl(
             @NonNull File icingDir,
             @NonNull AppSearchConfig config,
@@ -481,6 +485,7 @@
      * create a new, usable instance.
      */
     @Override
+    @OptIn(markerClass = ExperimentalAppSearchApi.class)
     public void close() {
         mReadWriteLock.writeLock().lock();
         try {
@@ -1145,6 +1150,10 @@
      * Gets the {@link ParcelFileDescriptor} for write purpose of the given
      * {@link AppSearchBlobHandle}.
      *
+     * <p> Only one opened {@link ParcelFileDescriptor} is allowed for each
+     * {@link AppSearchBlobHandle}. The same {@link ParcelFileDescriptor} will be returned if it is
+     * not closed by caller.
+     *
      * @param packageName    The package name that owns this blob.
      * @param databaseName   The databaseName this blob resides in.
      * @param handle         The {@link AppSearchBlobHandle} represent the blob.
@@ -1163,19 +1172,26 @@
         try {
             throwIfClosedLocked();
             verifyCallingBlobHandle(packageName, databaseName, handle);
+            ParcelFileDescriptor pfd = mRevocableFileDescriptorStore
+                    .getOpenedRevocableFileDescriptorForWrite(packageName, handle);
+            if (pfd != null) {
+                // There is already an opened pfd for write with same blob handle, just return the
+                // already opened one.
+                return pfd;
+            }
             mRevocableFileDescriptorStore.checkBlobStoreLimit(packageName);
             PropertyProto.BlobHandleProto blobHandleProto =
                     BlobHandleToProtoConverter.toBlobHandleProto(handle);
             BlobProto result = mIcingSearchEngineLocked.openWriteBlob(blobHandleProto);
 
             checkSuccess(result.getStatus());
-            ParcelFileDescriptor pfd = ParcelFileDescriptor.adoptFd(result.getFileDescriptor());
+            pfd = ParcelFileDescriptor.adoptFd(result.getFileDescriptor());
 
             mNamespaceCacheLocked.addToBlobNamespaceMap(createPrefix(packageName, databaseName),
                     blobHandleProto.getNamespace());
 
-            return mRevocableFileDescriptorStore
-                    .wrapToRevocableFileDescriptor(handle.getPackageName(), pfd);
+            return mRevocableFileDescriptorStore.wrapToRevocableFileDescriptor(
+                    packageName, handle, pfd, ParcelFileDescriptor.MODE_READ_WRITE);
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -1211,6 +1227,7 @@
                     BlobHandleToProtoConverter.toBlobHandleProto(handle));
 
             checkSuccess(result.getStatus());
+            mRevocableFileDescriptorStore.revokeFdForWrite(packageName, handle);
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -1230,7 +1247,7 @@
     public void commitBlob(
             @NonNull String packageName,
             @NonNull String databaseName,
-            @NonNull AppSearchBlobHandle handle) throws AppSearchException {
+            @NonNull AppSearchBlobHandle handle) throws AppSearchException, IOException {
         if (mRevocableFileDescriptorStore == null) {
             throw new UnsupportedOperationException(
                     "BLOB_STORAGE is not available on this AppSearch implementation.");
@@ -1243,6 +1260,8 @@
                     BlobHandleToProtoConverter.toBlobHandleProto(handle));
 
             checkSuccess(result.getStatus());
+            // The blob is committed and sealed, revoke the sent pfd for writing.
+            mRevocableFileDescriptorStore.revokeFdForWrite(packageName, handle);
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -1280,7 +1299,10 @@
             checkSuccess(result.getStatus());
 
             ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(result.getFileDescriptor());
-            return mRevocableFileDescriptorStore.wrapToRevocableFileDescriptor(packageName, pfd);
+            // We do NOT need to look up the revocable file descriptor for read, skip passing the
+            // blob handle key.
+            return mRevocableFileDescriptorStore.wrapToRevocableFileDescriptor(
+                    packageName, /*blobHandle=*/null, pfd, ParcelFileDescriptor.MODE_READ_ONLY);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -1330,8 +1352,13 @@
             checkSuccess(result.getStatus());
 
             ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(result.getFileDescriptor());
-            return mRevocableFileDescriptorStore
-                    .wrapToRevocableFileDescriptor(access.getCallingPackageName(), pfd);
+            // We do NOT need to look up the revocable file descriptor for read, skip passing the
+            // blob handle key.
+            return mRevocableFileDescriptorStore.wrapToRevocableFileDescriptor(
+                    access.getCallingPackageName(),
+                    /*blobHandle=*/null,
+                    pfd,
+                    ParcelFileDescriptor.MODE_READ_ONLY);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -2580,6 +2607,7 @@
      * @param packageName The name of package to be removed.
      * @throws AppSearchException if we cannot remove the data.
      */
+    @OptIn(markerClass = ExperimentalAppSearchApi.class)
     public void clearPackageData(@NonNull String packageName) throws AppSearchException,
             IOException {
         mReadWriteLock.writeLock().lock();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchRevocableFileDescriptor.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchRevocableFileDescriptor.java
new file mode 100644
index 0000000..bf0f134
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchRevocableFileDescriptor.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage;
+
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.OnCloseListener;
+
+import androidx.annotation.RestrictTo;
+
+import org.jspecify.annotations.NonNull;
+
+import java.io.IOException;
+
+/**
+ * A custom {@link ParcelFileDescriptor} that provides an additional mechanism to register
+ * a {@link ParcelFileDescriptor.OnCloseListener} which will be invoked when the file
+ * descriptor is closed.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppSearchRevocableFileDescriptor {
+
+    /**
+     * Gets the mode of this {@link AppSearchRevocableFileDescriptor}, It should be
+     * {@link ParcelFileDescriptor#MODE_READ_ONLY} or {@link ParcelFileDescriptor#MODE_READ_WRITE}.
+     */
+    int getMode();
+
+    /**
+     * Gets the revocable {@link ParcelFileDescriptor} that could be sent to an untrusted caller.
+     *
+     * <p> AppSearch will retain control of this {@link ParcelFileDescriptor}'s access to the file.
+     *
+     * <p> Call {@link #revoke()} to invoke the sent {@link ParcelFileDescriptor}.
+     */
+    @NonNull
+    ParcelFileDescriptor getRevocableFileDescriptor();
+
+    /**
+     * Revoke the sent {@link ParcelFileDescriptor} returned by
+     * {@link #getRevocableFileDescriptor()}.
+     *
+     * <p>After calling this method, any access to the file descriptors will fail.
+     *
+     * @throws IOException If an I/O error occurs while revoking file descriptors.
+     */
+    void revoke() throws IOException;
+
+    /**
+     * Callback for indicating that the {@link ParcelFileDescriptor} returned by
+     * {@link #getRevocableFileDescriptor()} has been closed.
+     */
+    void setOnCloseListener(@NonNull OnCloseListener onCloseListener);
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackAppSearchRevocableFileDescriptor.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackAppSearchRevocableFileDescriptor.java
new file mode 100644
index 0000000..c28a5d0
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackAppSearchRevocableFileDescriptor.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// @exportToFramework:copyToPath(../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/JetpackAppSearchRevocableFileDescriptor.java)
+
+package androidx.appsearch.localstorage;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import org.jspecify.annotations.NonNull;
+
+import java.io.IOException;
+
+/**
+ * The local storage implementation of {@link AppSearchRevocableFileDescriptor}.
+ *
+ * <p> Since the {@link ParcelFileDescriptor} sent to the client side from the local storage
+ * won't cross the binder, we could revoke the {@link ParcelFileDescriptor} in the client side
+ * by directly close the one in AppSearch side. This class just adding close listener to the
+ * inner {@link ParcelFileDescriptor}.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JetpackAppSearchRevocableFileDescriptor extends ParcelFileDescriptor
+        implements AppSearchRevocableFileDescriptor {
+    private ParcelFileDescriptor.OnCloseListener mOnCloseListener;
+    private int mMode;
+
+    /**
+     * Create a new ParcelFileDescriptor wrapped around another descriptor. By
+     * default all method calls are delegated to the wrapped descriptor.
+     */
+    JetpackAppSearchRevocableFileDescriptor(@NonNull ParcelFileDescriptor parcelFileDescriptor,
+            int mode) {
+        super(parcelFileDescriptor);
+        mMode = mode;
+    }
+
+    @Override
+    public int getMode() {
+        return mMode;
+    }
+
+    @Override
+    @NonNull
+    public ParcelFileDescriptor getRevocableFileDescriptor() {
+        return this;
+    }
+
+    @Override
+    public void setOnCloseListener(@NonNull OnCloseListener onCloseListener) {
+        if (mOnCloseListener != null) {
+            throw new IllegalStateException("The close listener has already been set.");
+        }
+        mOnCloseListener = Preconditions.checkNotNull(onCloseListener);
+    }
+
+    @Override
+    public void revoke() throws IOException {
+        // In jetpack, we already remove this revokeFd from cached map, we could directly close the
+        // super ParcelFileDescriptor without invoke close listener.
+        super.close();
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            super.close();
+            if (mOnCloseListener != null) {
+                // Success closed and invoke call back without exception.
+                mOnCloseListener.onClose(null);
+            }
+        } catch (IOException e) {
+            if (mOnCloseListener != null) {
+                mOnCloseListener.onClose(e);
+            }
+            throw e;
+        }
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackRevocableFileDescriptorStore.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackRevocableFileDescriptorStore.java
index 84013ac..b454028 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackRevocableFileDescriptorStore.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackRevocableFileDescriptorStore.java
@@ -19,20 +19,11 @@
 
 import android.os.ParcelFileDescriptor;
 
-import androidx.annotation.GuardedBy;
 import androidx.annotation.RestrictTo;
-import androidx.appsearch.app.AppSearchResult;
-import androidx.appsearch.exceptions.AppSearchException;
-import androidx.collection.ArrayMap;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
 
 import org.jspecify.annotations.NonNull;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
 /**
  * The local storage implementation of {@link RevocableFileDescriptorStore}.
  *
@@ -46,149 +37,18 @@
  * @exportToFramework:hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class JetpackRevocableFileDescriptorStore implements
+@ExperimentalAppSearchApi
+public class JetpackRevocableFileDescriptorStore extends
         RevocableFileDescriptorStore {
 
-    private final Object mLock  = new Object();
-    private final AppSearchConfig mConfig;
-
     public JetpackRevocableFileDescriptorStore(@NonNull AppSearchConfig config) {
-        mConfig = Preconditions.checkNotNull(config);
-    }
-
-    @GuardedBy("mLock")
-    // <package, List<sent rfds> map to tracking all sent rfds.
-    private final Map<String, List<JetpackRevocableFileDescriptor>>
-            mSentAppSearchParcelFileDescriptorsLocked = new ArrayMap<>();
-
-    @Override
-    public @NonNull ParcelFileDescriptor wrapToRevocableFileDescriptor(@NonNull String packageName,
-            @NonNull ParcelFileDescriptor parcelFileDescriptor) {
-        JetpackRevocableFileDescriptor revocableFileDescriptor =
-                new JetpackRevocableFileDescriptor(parcelFileDescriptor);
-        setCloseListenerToFd(revocableFileDescriptor, packageName);
-        addToSentAppSearchParcelFileDescriptorMap(revocableFileDescriptor, packageName);
-        return revocableFileDescriptor;
+        super(config);
     }
 
     @Override
-    public void revokeAll() throws IOException {
-        synchronized (mLock) {
-            for (String packageName : mSentAppSearchParcelFileDescriptorsLocked.keySet()) {
-                revokeForPackage(packageName);
-            }
-        }
-    }
-
-    @Override
-    public void revokeForPackage(@NonNull String packageName) throws IOException {
-        synchronized (mLock) {
-            List<JetpackRevocableFileDescriptor> rfds =
-                    mSentAppSearchParcelFileDescriptorsLocked.remove(packageName);
-            if (rfds != null) {
-                for (int i = rfds.size() - 1; i >= 0; i--) {
-                    rfds.get(i).closeSuperDirectly();
-                }
-            }
-        }
-    }
-
-    @Override
-    public void checkBlobStoreLimit(@NonNull String packageName) throws AppSearchException {
-        synchronized (mLock) {
-            List<JetpackRevocableFileDescriptor> rfdsForPackage =
-                    mSentAppSearchParcelFileDescriptorsLocked.get(packageName);
-            if (rfdsForPackage == null) {
-                return;
-            }
-            if (rfdsForPackage.size() >= mConfig.getMaxOpenBlobCount()) {
-                throw new AppSearchException(AppSearchResult.RESULT_OUT_OF_SPACE,
-                        "Package \"" + packageName + "\" exceeded limit of "
-                                + mConfig.getMaxOpenBlobCount()
-                                + " opened file descriptors. Some file descriptors "
-                                + "must be closed to open additional ones.");
-            }
-        }
-    }
-
-    private void setCloseListenerToFd(
-            @NonNull JetpackRevocableFileDescriptor revocableFileDescriptor,
-            @NonNull String packageName) {
-        revocableFileDescriptor.setCloseListener(e -> {
-            synchronized (mLock) {
-                List<JetpackRevocableFileDescriptor> fdsForPackage =
-                        mSentAppSearchParcelFileDescriptorsLocked.get(packageName);
-                if (fdsForPackage != null) {
-                    fdsForPackage.remove(revocableFileDescriptor);
-                    if (fdsForPackage.isEmpty()) {
-                        mSentAppSearchParcelFileDescriptorsLocked.remove(packageName);
-                    }
-                }
-            }
-        });
-    }
-
-    private void addToSentAppSearchParcelFileDescriptorMap(
-            @NonNull JetpackRevocableFileDescriptor revocableFileDescriptor,
-            @NonNull String packageName) {
-        synchronized (mLock) {
-            List<JetpackRevocableFileDescriptor> rfdsForPackage =
-                    mSentAppSearchParcelFileDescriptorsLocked.get(packageName);
-            if (rfdsForPackage == null) {
-                rfdsForPackage = new ArrayList<>();
-                mSentAppSearchParcelFileDescriptorsLocked.put(packageName, rfdsForPackage);
-            }
-            rfdsForPackage.add(revocableFileDescriptor);
-        }
-    }
-
-    /**
-     * A custom {@link ParcelFileDescriptor} that provides an additional mechanism to register
-     * a {@link ParcelFileDescriptor.OnCloseListener} which will be invoked when the file
-     * descriptor is closed.
-     *
-     * <p> Since the {@link ParcelFileDescriptor} sent to the client side from the local storage
-     * won't cross the binder, we could revoke the {@link ParcelFileDescriptor} in the client side
-     * by directly close the one in AppSearch side. This class just adding close listener to the
-     * inner {@link ParcelFileDescriptor}.
-     */
-    static class JetpackRevocableFileDescriptor extends ParcelFileDescriptor {
-        private ParcelFileDescriptor.OnCloseListener mOnCloseListener;
-
-        /**
-         * Create a new ParcelFileDescriptor wrapped around another descriptor. By
-         * default all method calls are delegated to the wrapped descriptor.
-         */
-        JetpackRevocableFileDescriptor(@NonNull ParcelFileDescriptor parcelFileDescriptor) {
-            super(parcelFileDescriptor);
-        }
-
-        void setCloseListener(
-                ParcelFileDescriptor.@NonNull OnCloseListener onCloseListener) {
-            if (mOnCloseListener != null) {
-                throw new IllegalStateException("The close listener has already been set.");
-            }
-            mOnCloseListener = Preconditions.checkNotNull(onCloseListener);
-        }
-
-        /**  Close the super {@link ParcelFileDescriptor} without invoke close listener.         */
-        void closeSuperDirectly() throws IOException {
-            super.close();
-        }
-
-        @Override
-        public void close() throws IOException {
-            try {
-                super.close();
-                if (mOnCloseListener != null) {
-                    mOnCloseListener.onClose(null);
-                }
-            } catch (IOException e) {
-                if (mOnCloseListener != null) {
-                    mOnCloseListener.onClose(e);
-                }
-                throw e;
-            }
-        }
+    protected @NonNull AppSearchRevocableFileDescriptor wrapToRevocableFileDescriptor(
+            @NonNull ParcelFileDescriptor parcelFileDescriptor,
+            int mode) {
+        return new JetpackAppSearchRevocableFileDescriptor(parcelFileDescriptor, mode);
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index d3377ee..d7d38b3 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -20,12 +20,14 @@
 import android.os.SystemClock;
 import android.util.Log;
 
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 import androidx.appsearch.annotation.Document;
 import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.flags.Flags;
@@ -359,6 +361,7 @@
     }
 
     @WorkerThread
+    @OptIn(markerClass = ExperimentalAppSearchApi.class)
     private LocalStorage(
             @NonNull Context context,
             @NonNull Executor executor,
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/RevocableFileDescriptorStore.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/RevocableFileDescriptorStore.java
index 200b5b7..12ecb4b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/RevocableFileDescriptorStore.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/RevocableFileDescriptorStore.java
@@ -18,39 +18,135 @@
 
 import android.os.ParcelFileDescriptor;
 
+
+import androidx.annotation.GuardedBy;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBlobHandle;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
 
 import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
- * Interface for revocable file descriptors storage.
+ * The base class for revocable file descriptors storage.
  *
  * <p>This store allows wrapping {@link ParcelFileDescriptor} instances into revocable file
  * descriptors, enabling the ability to close and revoke it's access to the file even if the
  * {@link ParcelFileDescriptor} has been sent to the client side.
  *
- * <p> Implementations of this interface can provide controlled access to resources by associating
- * each file descriptor with a package and allowing them to be individually revoked by package
- * or revoked all at once.
+ * <p> This class can provide controlled access to resources by associating each file descriptor
+ * with a package and allowing them to be individually revoked by package or revoked all at once.
+ *
+ * <p> The sub-class must define how to wrap a {@link ParcelFileDescriptor} to a
+ * {@link AppSearchRevocableFileDescriptor}.
+ *
+ * <p> This class stores {@link AppSearchBlobHandle} and returned
+ * {@link AppSearchRevocableFileDescriptor} for writing in key-value pairs map. Only one opened
+ * {@link AppSearchRevocableFileDescriptor} for writing will be allowed for each
+ * {@link AppSearchBlobHandle}.
+ *
+ * <p> This class stores {@link AppSearchRevocableFileDescriptor} for reading in a list. There is no
+ * use case to look up and invoke a single {@link AppSearchRevocableFileDescriptor} for reading.
  *
  * @exportToFramework:hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface RevocableFileDescriptorStore {
+@ExperimentalAppSearchApi
+public abstract class RevocableFileDescriptorStore {
+
+    private final Object mLock  = new Object();
+    private final AppSearchConfig mConfig;
+
+    public RevocableFileDescriptorStore(@NonNull AppSearchConfig config) {
+        mConfig = Preconditions.checkNotNull(config);
+    }
+
+    @GuardedBy("mLock")
+    // Map<package, Map<blob handle, sent rfds>> map to track all sent rfds for writing. We only
+    // allow user to open 1 pfd for write for same file.
+    private final Map<String, Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor>>
+            mSentRevocableFileDescriptorsForWriteLocked = new ArrayMap<>();
+
+    @GuardedBy("mLock")
+    // <package, List<sent rfds> map to track all sent rfds for reading. We allow opening
+    // multiple pfds for read for the same file.
+    private final Map<String, List<AppSearchRevocableFileDescriptor>>
+            mSentRevocableFileDescriptorsForReadLocked = new ArrayMap<>();
 
     /**
-     * Wraps the provided ParcelFileDescriptor into a revocable file descriptor.
-     * This allows for controlled access to the file descriptor, making it revocable by the store.
+     * Wraps the provided {@link ParcelFileDescriptor} into a revocable file descriptor.
+     *
+     * <p>This allows for controlled access to the file descriptor, making it revocable by the
+     * store.
      *
      * @param packageName The package name requesting the revocable file descriptor.
-     * @param parcelFileDescriptor The original ParcelFileDescriptor to be wrapped.
-     * @return A ParcelFileDescriptor that can be revoked by the store.
+     * @param blobHandle The blob handle associated with the file descriptor. It cannot be null if
+     *                   the mode is READ_WRITE.
+     * @param parcelFileDescriptor The original {@link ParcelFileDescriptor} to be wrapped.
+     * @param mode  The mode of the given {@link ParcelFileDescriptor}. It should be
+     * {@link ParcelFileDescriptor#MODE_READ_ONLY} or {@link ParcelFileDescriptor#MODE_READ_WRITE}.
+     * @return A {@link ParcelFileDescriptor} that can be revoked by the store.
+     * @throws IOException if an I/O error occurs while creating the revocable file descriptor.
      */
-    @NonNull ParcelFileDescriptor wrapToRevocableFileDescriptor(@NonNull String packageName,
-            @NonNull ParcelFileDescriptor parcelFileDescriptor) throws IOException;
+    public  @NonNull ParcelFileDescriptor wrapToRevocableFileDescriptor(
+            @NonNull String packageName,
+            @Nullable AppSearchBlobHandle blobHandle,
+            @NonNull ParcelFileDescriptor parcelFileDescriptor,
+            int mode) throws IOException {
+        AppSearchRevocableFileDescriptor revocableFileDescriptor =
+                wrapToRevocableFileDescriptor(parcelFileDescriptor, mode);
+        setCloseListenerToFd(packageName, blobHandle, revocableFileDescriptor);
+        addToSentRevocableFileDescriptorMap(packageName, blobHandle,
+                revocableFileDescriptor);
+        return revocableFileDescriptor.getRevocableFileDescriptor();
+    }
+
+    /**
+     * Wraps the provided {@link ParcelFileDescriptor} into a specific type of
+     * {@link AppSearchRevocableFileDescriptor}.
+     */
+    protected abstract @NonNull AppSearchRevocableFileDescriptor wrapToRevocableFileDescriptor(
+            @NonNull ParcelFileDescriptor parcelFileDescriptor,
+            int mode) throws IOException;
+
+    /**
+     * Gets the opened revocable file descriptor for write associated with the given
+     * {@link AppSearchBlobHandle}.
+     *
+     * @param packageName The package name associated with the file descriptor.
+     * @param blobHandle The blob handle associated with the file descriptor.
+     * @return The opened revocable file descriptor, or {@code null} if not found.
+     */
+    public @Nullable ParcelFileDescriptor getOpenedRevocableFileDescriptorForWrite(
+            @NonNull String packageName,
+            @NonNull AppSearchBlobHandle blobHandle) {
+        synchronized (mLock) {
+            Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdsForPackage =
+                    mSentRevocableFileDescriptorsForWriteLocked.get(packageName);
+            if (rfdsForPackage == null) {
+                return null;
+            }
+            AppSearchRevocableFileDescriptor revocableFileDescriptor =
+                    rfdsForPackage.get(blobHandle);
+            if (revocableFileDescriptor == null) {
+                return null;
+            }
+            // The revocableFileDescriptor should never be revoked, otherwise it should be removed
+            // from the map.
+            return revocableFileDescriptor.getRevocableFileDescriptor();
+        }
+    }
 
     /**
      * Revokes all revocable file descriptors previously issued by the store.
@@ -58,7 +154,16 @@
      *
      * @throws IOException If an I/O error occurs while revoking file descriptors.
      */
-    void revokeAll() throws IOException;
+    public void revokeAll() throws IOException {
+        synchronized (mLock) {
+            Set<String> packageNames =
+                    new ArraySet<>(mSentRevocableFileDescriptorsForReadLocked.keySet());
+            packageNames.addAll(mSentRevocableFileDescriptorsForWriteLocked.keySet());
+            for (String packageName : packageNames) {
+                revokeForPackage(packageName);
+            }
+        }
+    }
 
     /**
      * Revokes all revocable file descriptors for a specified package.
@@ -67,8 +172,184 @@
      * @param packageName The package name whose file descriptors should be revoked.
      * @throws IOException If an I/O error occurs while revoking file descriptors.
      */
-    void revokeForPackage(@NonNull String packageName) throws IOException;
+    public void revokeForPackage(@NonNull String packageName) throws IOException {
+        synchronized (mLock) {
+            List<AppSearchRevocableFileDescriptor> rfdsForRead =
+                    mSentRevocableFileDescriptorsForReadLocked.remove(packageName);
+            if (rfdsForRead != null) {
+                for (int i = rfdsForRead.size() - 1; i >= 0; i--) {
+                    rfdsForRead.get(i).revoke();
+                }
+            }
+
+            Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdsForWrite =
+                    mSentRevocableFileDescriptorsForWriteLocked.remove(packageName);
+            if (rfdsForWrite != null) {
+                for (AppSearchRevocableFileDescriptor rfdForWrite : rfdsForWrite.values()) {
+                    rfdForWrite.revoke();
+                }
+            }
+        }
+    }
+
+    /**
+     * Revokes the revocable file descriptors for write associated with the given
+     * {@link AppSearchBlobHandle}.
+     *
+     * <p> Once a blob is sealed, we should call this method to revoke the sent file descriptor for
+     * write. Otherwise, the user could keep writing to the committed file.
+     *
+     * @param packageName The package name whose file descriptors should be revoked.
+     * @param blobHandle  The blob handle associated with the file descriptors.
+     * @throws IOException If an I/O error occurs while revoking file descriptors.
+     */
+    public void revokeFdForWrite(@NonNull String packageName,
+            @NonNull AppSearchBlobHandle blobHandle) throws IOException {
+        synchronized (mLock) {
+            Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdsForWrite =
+                    mSentRevocableFileDescriptorsForWriteLocked.remove(packageName);
+            if (rfdsForWrite == null) {
+                return;
+            }
+            AppSearchRevocableFileDescriptor revocableFileDescriptor =
+                    rfdsForWrite.remove(blobHandle);
+            if (revocableFileDescriptor == null) {
+                return;
+            }
+            revocableFileDescriptor.revoke();
+            if (rfdsForWrite.isEmpty()) {
+                mSentRevocableFileDescriptorsForWriteLocked.remove(packageName);
+            }
+        }
+    }
 
     /**  Checks if the specified package has reached its blob storage limit. */
-    void checkBlobStoreLimit(@NonNull String packageName) throws AppSearchException;
+    public void checkBlobStoreLimit(@NonNull String packageName) throws AppSearchException {
+        synchronized (mLock) {
+            int totalOpenFdSize = 0;
+            List<AppSearchRevocableFileDescriptor> rfdsForRead =
+                    mSentRevocableFileDescriptorsForReadLocked.get(packageName);
+            if (rfdsForRead != null) {
+                totalOpenFdSize += rfdsForRead.size();
+            }
+            Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdsForWrite =
+                    mSentRevocableFileDescriptorsForWriteLocked.get(packageName);
+            if (rfdsForWrite != null) {
+                totalOpenFdSize += rfdsForWrite.size();
+            }
+            if (totalOpenFdSize >= mConfig.getMaxOpenBlobCount()) {
+                throw new AppSearchException(AppSearchResult.RESULT_OUT_OF_SPACE,
+                        "Package \"" + packageName + "\" exceeded limit of "
+                                + mConfig.getMaxOpenBlobCount()
+                                + " opened file descriptors. Some file descriptors "
+                                + "must be closed to open additional ones.");
+            }
+        }
+    }
+
+    /**
+     * Sets a close listener to the revocable file descriptor for write.
+     *
+     * <p>The listener will be invoked when the file descriptor is closed.
+     *
+     * @param packageName The package name associated with the file descriptor.
+     * @param blobHandle The blob handle associated with the file descriptor. It cannot be null if
+     *                   the mode is READ_WRITE.
+     * @param revocableFileDescriptor The revocable file descriptor to set the listener to.
+     */
+    private void setCloseListenerToFd(
+            @NonNull String packageName,
+            @Nullable AppSearchBlobHandle blobHandle,
+            @NonNull AppSearchRevocableFileDescriptor revocableFileDescriptor) {
+        ParcelFileDescriptor.OnCloseListener closeListener;
+        switch (revocableFileDescriptor.getMode()) {
+            case ParcelFileDescriptor.MODE_READ_ONLY:
+                closeListener = e -> {
+                    synchronized (mLock) {
+                        List<AppSearchRevocableFileDescriptor> fdsForPackage =
+                                mSentRevocableFileDescriptorsForReadLocked.get(packageName);
+                        if (fdsForPackage != null) {
+                            fdsForPackage.remove(revocableFileDescriptor);
+                            if (fdsForPackage.isEmpty()) {
+                                mSentRevocableFileDescriptorsForReadLocked.remove(packageName);
+                            }
+                        }
+                    }
+                };
+                break;
+            case ParcelFileDescriptor.MODE_READ_WRITE:
+                Preconditions.checkNotNull(blobHandle);
+                closeListener = e -> {
+                    synchronized (mLock) {
+                        Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor>
+                                rfdsForPackage = mSentRevocableFileDescriptorsForWriteLocked
+                                .get(packageName);
+                        if (rfdsForPackage != null) {
+                            AppSearchRevocableFileDescriptor rfd =
+                                    rfdsForPackage.remove(blobHandle);
+                            if (rfd != null) {
+                                try {
+                                    rfd.revoke();
+                                } catch (IOException ioException) {
+                                    // ignore, the sent RevocableFileDescriptor should already
+                                    // be closed.
+                                }
+                            }
+                            if (rfdsForPackage.isEmpty()) {
+                                mSentRevocableFileDescriptorsForWriteLocked.remove(packageName);
+                            }
+                        }
+                    }
+                };
+                break;
+            default:
+                throw new UnsupportedOperationException(
+                        "Cannot support the AppSearchRevocableFileDescriptor mode: "
+                                + revocableFileDescriptor.getMode());
+        }
+        revocableFileDescriptor.setOnCloseListener(closeListener);
+    }
+
+    /**
+     * Adds a revocable file descriptor to the sent revocable file descriptor map.
+     *
+     * @param packageName The package name associated with the file descriptor.
+     * @param blobHandle The blob handle associated with the file descriptor. It cannot be null if
+     *                   the mode is READ_WRITE.
+     * @param revocableFileDescriptor The revocable file descriptor to add.
+     */
+    private void addToSentRevocableFileDescriptorMap(
+            @NonNull String packageName,
+            @Nullable AppSearchBlobHandle blobHandle,
+            @NonNull AppSearchRevocableFileDescriptor revocableFileDescriptor) {
+        synchronized (mLock) {
+            switch (revocableFileDescriptor.getMode()) {
+                case ParcelFileDescriptor.MODE_READ_ONLY:
+                    List<AppSearchRevocableFileDescriptor> rfdListForPackage =
+                            mSentRevocableFileDescriptorsForReadLocked.get(packageName);
+                    if (rfdListForPackage == null) {
+                        rfdListForPackage = new ArrayList<>();
+                        mSentRevocableFileDescriptorsForReadLocked.put(packageName,
+                                rfdListForPackage);
+                    }
+                    rfdListForPackage.add(revocableFileDescriptor);
+                    break;
+                case ParcelFileDescriptor.MODE_READ_WRITE:
+                    Preconditions.checkNotNull(blobHandle);
+                    Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdMapForPackage =
+                            mSentRevocableFileDescriptorsForWriteLocked.get(packageName);
+                    if (rfdMapForPackage == null) {
+                        rfdMapForPackage = new ArrayMap<>();
+                        mSentRevocableFileDescriptorsForWriteLocked.put(packageName,
+                                rfdMapForPackage);
+                    }
+                    rfdMapForPackage.put(blobHandle, revocableFileDescriptor);
+                    break;
+                default:
+                    throw new UnsupportedOperationException(
+                            "Cannot support the AppSearchRevocableFileDescriptor mode: "
+                                    + revocableFileDescriptor.getMode());
+            }
+        }
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionBlobCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionBlobCtsTestBase.java
index fd4cb7a..17621bf 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionBlobCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionBlobCtsTestBase.java
@@ -62,6 +62,7 @@
 import org.junit.Test;
 import org.junit.rules.RuleChain;
 
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -173,6 +174,38 @@
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
+    public void testWriteAfterCommit() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.BLOB_STORAGE));
+
+        OpenBlobForWriteResponse writeResponse =
+                mDb1.openBlobForWriteAsync(ImmutableSet.of(mHandle1)).get();
+        AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor> writeResult =
+                writeResponse.getResult();
+        assertTrue(writeResult.isSuccess());
+
+        // Write data without close the pfd for write
+        ParcelFileDescriptor writePfd = writeResult.getSuccesses().get(mHandle1);
+        try (FileOutputStream outputStream = new FileOutputStream(writePfd.getFileDescriptor())) {
+            outputStream.write(mData1);
+            outputStream.flush();
+        }
+
+        // Commit the blob will revoke the pfd for write.
+        assertTrue(mDb1.commitBlobAsync(ImmutableSet.of(mHandle1)).get().getResult()
+                .isSuccess());
+
+        // Cannot keep writing to the blob after commit.
+        assertThrows(IOException.class, () -> {
+            try (FileOutputStream outputStream =
+                         new FileOutputStream(writePfd.getFileDescriptor())) {
+                outputStream.write(mData1);
+                outputStream.flush();
+            }
+        });
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
     public void testRemovePendingBlob() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.BLOB_STORAGE));
 
diff --git a/benchmark/benchmark-common/api/current.txt b/benchmark/benchmark-common/api/current.txt
index f1f1bfe..44c9777 100644
--- a/benchmark/benchmark-common/api/current.txt
+++ b/benchmark/benchmark-common/api/current.txt
@@ -2,20 +2,13 @@
 package androidx.benchmark {
 
   public final class BenchmarkState {
-    ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public BenchmarkState(optional Integer? warmupCount, optional Integer? repeatCount);
-    method @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public java.util.List<java.lang.Double> getMeasurementTimeNs();
     method public boolean keepRunning();
     method public void pauseTiming();
-    method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public static void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
     method public void resumeTiming();
     field public static final androidx.benchmark.BenchmarkState.Companion Companion;
   }
 
   public static final class BenchmarkState.Companion {
-    method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public static @interface BenchmarkState.Companion.ExperimentalExternalReport {
   }
 
   @SuppressCompatibility @androidx.benchmark.ExperimentalBlackHoleApi public final class BlackHole {
@@ -61,15 +54,19 @@
 
   @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class MicrobenchmarkConfig {
     ctor public MicrobenchmarkConfig();
-    ctor public MicrobenchmarkConfig(optional java.util.List<? extends androidx.benchmark.MetricCapture> metrics, optional boolean traceAppTagEnabled, optional boolean perfettoSdkTracingEnabled, optional androidx.benchmark.ProfilerConfig? profiler);
+    ctor public MicrobenchmarkConfig(optional java.util.List<? extends androidx.benchmark.MetricCapture> metrics, optional boolean traceAppTagEnabled, optional boolean perfettoSdkTracingEnabled, optional androidx.benchmark.ProfilerConfig? profiler, optional Integer? warmupCount, optional Integer? measurementCount);
+    method public Integer? getMeasurementCount();
     method public java.util.List<androidx.benchmark.MetricCapture> getMetrics();
     method public androidx.benchmark.ProfilerConfig? getProfiler();
+    method public Integer? getWarmupCount();
     method public boolean isPerfettoSdkTracingEnabled();
     method public boolean isTraceAppTagEnabled();
+    property public final Integer? measurementCount;
     property public final java.util.List<androidx.benchmark.MetricCapture> metrics;
     property public final boolean perfettoSdkTracingEnabled;
     property public final androidx.benchmark.ProfilerConfig? profiler;
     property public final boolean traceAppTagEnabled;
+    property public final Integer? warmupCount;
   }
 
   @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract sealed class ProfilerConfig {
diff --git a/benchmark/benchmark-common/api/restricted_current.txt b/benchmark/benchmark-common/api/restricted_current.txt
index 7271b61..f25ea8e 100644
--- a/benchmark/benchmark-common/api/restricted_current.txt
+++ b/benchmark/benchmark-common/api/restricted_current.txt
@@ -2,12 +2,9 @@
 package androidx.benchmark {
 
   public final class BenchmarkState {
-    ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public BenchmarkState(optional Integer? warmupCount, optional Integer? repeatCount);
-    method @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public java.util.List<java.lang.Double> getMeasurementTimeNs();
     method public boolean keepRunning();
     method @kotlin.PublishedApi internal boolean keepRunningInternal();
     method public void pauseTiming();
-    method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public static void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
     method public void resumeTiming();
     property @kotlin.PublishedApi internal final int iterationsRemaining;
     field public static final androidx.benchmark.BenchmarkState.Companion Companion;
@@ -15,10 +12,6 @@
   }
 
   public static final class BenchmarkState.Companion {
-    method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public static @interface BenchmarkState.Companion.ExperimentalExternalReport {
   }
 
   @SuppressCompatibility @androidx.benchmark.ExperimentalBlackHoleApi public final class BlackHole {
@@ -64,15 +57,19 @@
 
   @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class MicrobenchmarkConfig {
     ctor public MicrobenchmarkConfig();
-    ctor public MicrobenchmarkConfig(optional java.util.List<? extends androidx.benchmark.MetricCapture> metrics, optional boolean traceAppTagEnabled, optional boolean perfettoSdkTracingEnabled, optional androidx.benchmark.ProfilerConfig? profiler);
+    ctor public MicrobenchmarkConfig(optional java.util.List<? extends androidx.benchmark.MetricCapture> metrics, optional boolean traceAppTagEnabled, optional boolean perfettoSdkTracingEnabled, optional androidx.benchmark.ProfilerConfig? profiler, optional Integer? warmupCount, optional Integer? measurementCount);
+    method public Integer? getMeasurementCount();
     method public java.util.List<androidx.benchmark.MetricCapture> getMetrics();
     method public androidx.benchmark.ProfilerConfig? getProfiler();
+    method public Integer? getWarmupCount();
     method public boolean isPerfettoSdkTracingEnabled();
     method public boolean isTraceAppTagEnabled();
+    property public final Integer? measurementCount;
     property public final java.util.List<androidx.benchmark.MetricCapture> metrics;
     property public final boolean perfettoSdkTracingEnabled;
     property public final androidx.benchmark.ProfilerConfig? profiler;
     property public final boolean traceAppTagEnabled;
+    property public final Integer? warmupCount;
   }
 
   @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract sealed class ProfilerConfig {
diff --git a/benchmark/benchmark-common/build.gradle b/benchmark/benchmark-common/build.gradle
index d1537e3..e22a1e6 100644
--- a/benchmark/benchmark-common/build.gradle
+++ b/benchmark/benchmark-common/build.gradle
@@ -85,6 +85,7 @@
     implementation("androidx.test:monitor:1.6.1")
     implementation(libs.wireRuntime)
     implementation(libs.moshi)
+    implementation(libs.kotlinCoroutinesAndroid)
     ksp(libs.moshiCodeGen)
 
     androidTestImplementation(libs.testRules)
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateLegacyConfigTest.kt
similarity index 98%
rename from benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
rename to benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateLegacyConfigTest.kt
index bc0ea41..0fa1ce6 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateLegacyConfigTest.kt
@@ -27,7 +27,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class BenchmarkStateConfigTest {
+class BenchmarkStateLegacyConfigTest {
     private fun validateConfig(
         config: MicrobenchmarkPhase.Config,
         expectedWarmups: Int?,
@@ -36,7 +36,7 @@
         expectedUsesProfiler: Boolean = false,
         expectedProfilerIterations: Int = 0
     ) {
-        val state = BenchmarkState(config)
+        val state = BenchmarkStateLegacy(config)
         var count = 0
         while (state.keepRunning()) {
             // This spin loop works around an issue where nanoTime is only precise to 30us on some
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateLegacyTest.kt
similarity index 85%
rename from benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
rename to benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateLegacyTest.kt
index fc41186..616b446 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateLegacyTest.kt
@@ -18,8 +18,6 @@
 
 import android.Manifest
 import androidx.annotation.RequiresApi
-import androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport
-import androidx.benchmark.json.BenchmarkData
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
@@ -39,7 +37,7 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-class BenchmarkStateTest {
+class BenchmarkStateLegacyTest {
     private fun us2ns(ms: Long): Long = TimeUnit.MICROSECONDS.toNanos(ms)
 
     @get:Rule
@@ -63,7 +61,7 @@
     @Test
     @FlakyTest(bugId = 187711141)
     fun validateMetrics() {
-        val state = BenchmarkState()
+        val state = BenchmarkStateLegacy()
         while (state.keepRunning()) {
             runAndSpin(durationUs = 300) {
                 // note, important here to not do too much work - this test may run on an
@@ -88,7 +86,7 @@
 
     @Test
     fun keepRunningMissingResume() {
-        val state = BenchmarkState()
+        val state = BenchmarkStateLegacy()
 
         assertEquals(true, state.keepRunning())
         state.pauseTiming()
@@ -97,7 +95,7 @@
 
     @Test
     fun pauseCalledTwice() {
-        val state = BenchmarkState()
+        val state = BenchmarkStateLegacy()
 
         assertEquals(true, state.keepRunning())
         state.pauseTiming()
@@ -114,7 +112,7 @@
         )
 
         // verify priority is only bumped during loop (NOTE: lower number means higher priority)
-        val state = BenchmarkState()
+        val state = BenchmarkStateLegacy()
         while (state.keepRunning()) {
             val currentJitPriority = ThreadPriority.getJit()
             assertTrue(
@@ -136,7 +134,7 @@
         )
 
         // verify priority is only bumped during loop (NOTE: lower number means higher priority)
-        val state = BenchmarkState()
+        val state = BenchmarkStateLegacy()
         while (state.keepRunning()) {
             val currentPriority = ThreadPriority.get()
             assertTrue(
@@ -150,7 +148,7 @@
     private fun iterationCheck(simplifiedTimingOnlyMode: Boolean) {
         // disable thermal throttle checks, since it can cause loops to be thrown out
         // note that this bypasses allocation count
-        val state = BenchmarkState(simplifiedTimingOnlyMode = simplifiedTimingOnlyMode)
+        val state = BenchmarkStateLegacy(simplifiedTimingOnlyMode = simplifiedTimingOnlyMode)
         var total = 0
         while (state.keepRunning()) {
             total++
@@ -161,7 +159,7 @@
         // '50' assumes we're not running in a special mode
         // that affects repeat count (dry run)
         val expectedRepeatCount =
-            50 + if (simplifiedTimingOnlyMode) 0 else BenchmarkState.REPEAT_COUNT_ALLOCATION
+            50 + if (simplifiedTimingOnlyMode) 0 else BenchmarkStateLegacy.REPEAT_COUNT_ALLOCATION
         val expectedCount =
             testResult.warmupIterations!! +
                 testResult.repeatIterations!! * expectedRepeatCount +
@@ -198,7 +196,7 @@
     @Suppress("DEPRECATION")
     fun bundle() {
         val bundle =
-            BenchmarkState()
+            BenchmarkStateLegacy()
                 .apply {
                     while (keepRunning()) {
                         // nothing, we're ignoring numbers
@@ -236,7 +234,7 @@
     fun notStarted() {
         val initialPriority = ThreadPriority.get()
         try {
-            BenchmarkState().peekTestResult().metrics["timeNs"]!!.median
+            BenchmarkStateLegacy().peekTestResult().metrics["timeNs"]!!.median
             fail("expected exception")
         } catch (e: IllegalStateException) {
             assertEquals(initialPriority, ThreadPriority.get())
@@ -248,7 +246,7 @@
     fun notFinished() {
         val initialPriority = ThreadPriority.get()
         try {
-            BenchmarkState().run {
+            BenchmarkStateLegacy().run {
                 keepRunning()
                 peekTestResult().metrics["timeNs"]!!.median
             }
@@ -260,48 +258,25 @@
         }
     }
 
-    @OptIn(ExperimentalExternalReport::class)
-    @Test
-    fun reportResult() {
-        BenchmarkState.reportData(
-            className = "className",
-            testName = "testName",
-            totalRunTimeNs = 900000000,
-            dataNs = listOf(100L, 200L, 300L),
-            warmupIterations = 1,
-            thermalThrottleSleepSeconds = 0,
-            repeatIterations = 1
-        )
-        val expectedReport =
-            BenchmarkData.TestResult(
-                className = "className",
-                name = "testName",
-                totalRunTimeNs = 900000000,
-                metrics = listOf(MetricResult(name = "timeNs", data = listOf(100.0, 200.0, 300.0))),
-                repeatIterations = 1,
-                thermalThrottleSleepSeconds = 0,
-                warmupIterations = 1,
-                profilerOutputs = null,
-            )
-        assertEquals(expectedReport, ResultWriter.reports.last())
-    }
-
     @RequiresApi(22) // 21 profiler has flaky platform crashes, see b/353716346
     private fun validateProfilerUsage(simplifiedTimingOnlyMode: Boolean?) {
         val config = MicrobenchmarkConfig(profiler = ProfilerConfig.StackSamplingLegacy())
 
-        val benchmarkState =
+        val benchmarkStateLegacy =
             if (simplifiedTimingOnlyMode != null) {
-                BenchmarkState(config = config, simplifiedTimingOnlyMode = simplifiedTimingOnlyMode)
+                BenchmarkStateLegacy(
+                    config = config,
+                    simplifiedTimingOnlyMode = simplifiedTimingOnlyMode
+                )
             } else {
-                BenchmarkState(config)
+                BenchmarkStateLegacy(config)
             }
 
         // count iters with profiler enabled vs disabled
         var profilerDisabledIterations = 0
         var profilerEnabledIterations = 0
         var profilerAllocationIterations = 0
-        while (benchmarkState.keepRunning()) {
+        while (benchmarkStateLegacy.keepRunning()) {
             if (StackSamplingLegacy.isRunning) {
                 profilerEnabledIterations++
             } else {
@@ -345,10 +320,10 @@
     @Test
     fun experimentalConstructor() {
         // min values that don't fail
-        BenchmarkState(warmupCount = null, measurementCount = 1)
+        BenchmarkStateLegacy(warmupCount = null, measurementCount = 1)
 
         // test failures
-        assertFailsWith<IllegalArgumentException> { BenchmarkState(warmupCount = 0) }
-        assertFailsWith<IllegalArgumentException> { BenchmarkState(measurementCount = 0) }
+        assertFailsWith<IllegalArgumentException> { BenchmarkStateLegacy(warmupCount = 0) }
+        assertFailsWith<IllegalArgumentException> { BenchmarkStateLegacy(measurementCount = 0) }
     }
 }
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MicrobenchmarkPhaseConfigTest.kt
similarity index 74%
copy from benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
copy to benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MicrobenchmarkPhaseConfigTest.kt
index bc0ea41..5fea55f 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MicrobenchmarkPhaseConfigTest.kt
@@ -22,12 +22,13 @@
 import androidx.test.filters.SmallTest
 import kotlin.test.assertEquals
 import kotlin.test.assertNotEquals
+import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class BenchmarkStateConfigTest {
+class MicrobenchmarkPhaseConfigTest {
     private fun validateConfig(
         config: MicrobenchmarkPhase.Config,
         expectedWarmups: Int?,
@@ -36,18 +37,39 @@
         expectedUsesProfiler: Boolean = false,
         expectedProfilerIterations: Int = 0
     ) {
-        val state = BenchmarkState(config)
         var count = 0
-        while (state.keepRunning()) {
-            // This spin loop works around an issue where nanoTime is only precise to 30us on some
-            // devices. This was reproduced on api 17 and emulators api 33. (b/331226761)
-            val start = System.nanoTime()
-            @Suppress("ControlFlowWithEmptyBody") while (System.nanoTime() == start) {}
-            count++
+        val output = runBlocking {
+            val microbenchmark =
+                Microbenchmark(
+                    TestDefinition(
+                        "MicrobenchmarkPhaseConfigTest",
+                        "MicrobenchmarkPhaseConfigTest",
+                        "methodName"
+                    ),
+                    phaseConfig = config,
+                    yieldThreadPeriodically = false,
+                    scopeFactory = { state: MicrobenchmarkRunningState ->
+                        MicrobenchmarkScope(state)
+                    },
+                    loopedMeasurementBlock = { _, loops ->
+                        repeat(loops) {
+                            // This spin loop works around an issue where nanoTime is only precise
+                            // to 30us on some
+                            // devices. This was reproduced on api 17 and emulators api 33.
+                            // (b/331226761)
+                            val start = System.nanoTime()
+                            @Suppress("ControlFlowWithEmptyBody")
+                            while (System.nanoTime() == start) {}
+                            count++
+                        }
+                    }
+                )
+            microbenchmark.executePhases()
+            microbenchmark.output(null)
         }
 
         val calculatedIterations =
-            state.warmupRepeats + expectedMeasurements * state.iterationsPerRepeat
+            output.warmupIterations + expectedMeasurements * output.repeatIterations
 
         val usesProfiler = config.generatePhases().any { it.profiler != null }
 
@@ -63,9 +85,11 @@
             assertEquals(expectedIterations, count)
         }
         if (expectedWarmups != null) {
-            assertEquals(expectedWarmups, state.warmupRepeats)
+            assertEquals(expectedWarmups, output.warmupIterations)
         }
-        assertNotEquals(0.0, state.getMinTimeNanos()) // just verify some value is set
+
+        val minNanos = output.metricResults.single { it.name == "timeNs" }.min
+        assertNotEquals(0.0, minNanos) // just verify some value is set
     }
 
     @Test
@@ -141,6 +165,24 @@
             expectedIterations = null, // iterations are dynamic
         )
 
+    @Test
+    fun trivial() =
+        validateConfig(
+            MicrobenchmarkPhase.Config(
+                dryRunMode = false,
+                startupMode = false,
+                simplifiedTimingOnlyMode = false,
+                profiler = null,
+                profilerPerfCompareMode = false,
+                warmupCount = 3,
+                measurementCount = 2,
+                metrics = arrayOf(TimeCapture()),
+            ),
+            expectedWarmups = 3,
+            expectedMeasurements = 7, // includes allocations
+            expectedIterations = null, // iterations are dynamic
+        )
+
     @SdkSuppress(minSdkVersion = 22) // See b/300658578
     @Test
     fun profilerMethodTracing() =
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
index c352a21..ad3e72c 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -28,8 +28,9 @@
 @get:RestrictTo(RestrictTo.Scope.LIBRARY)
 @set:RestrictTo(RestrictTo.Scope.LIBRARY)
 @VisibleForTesting
-public var argumentSource: Bundle? = null
+var argumentSource: Bundle? = null
 
+@Suppress("NullableBooleanElvis") // suggestion makes boolean argument defaults less clear
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 object Arguments {
     // public properties are shared by micro + macro benchmarks
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkState.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkState.kt
index 3c0e979..8114e29 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkState.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkState.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -16,25 +16,93 @@
 
 package androidx.benchmark
 
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.os.Looper
-import android.util.Log
-import androidx.annotation.IntRange
 import androidx.annotation.RestrictTo
-import androidx.annotation.VisibleForTesting
-import androidx.benchmark.Errors.PREFIX
-import androidx.benchmark.InstrumentationResults.instrumentationReport
-import androidx.benchmark.InstrumentationResults.reportBundle
-import androidx.benchmark.json.BenchmarkData
 import java.util.concurrent.TimeUnit
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
+import kotlin.coroutines.intrinsics.createCoroutineUnintercepted
+import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
+import kotlin.coroutines.resume
 
 /**
- * Control object for benchmarking in the code in Java.
+ * This function is used to allow BenchmarkState to provide its non-suspending keepRunning(), but
+ * underneath incrementally progress the underlying coroutine microbenchmark API as needed.
+ *
+ * It is optimized to allow the underlying coroutine API to be incrementally upgraded without any
+ * changes to BenchmarkState.
+ *
+ * This is modeled after the suspending iterator {} sequence builder in kotlin, but optimized for:
+ * - minimal allocation
+ * - keeping yields:resumes at 1:1 (for simplicity)
+ * - minimal virtual functions
+ *
+ * @see kotlin.sequences.iterator
+ */
+private fun createSuspendedLoop(
+    block: suspend SuspendedLoopTrigger.() -> Unit
+): SuspendedLoopTrigger {
+    val suspendedLoopTrigger = SuspendedLoopTrigger()
+    suspendedLoopTrigger.nextStep =
+        block.createCoroutineUnintercepted(
+            receiver = suspendedLoopTrigger,
+            completion = suspendedLoopTrigger
+        )
+    return suspendedLoopTrigger
+}
+
+/**
+ * SuspendedLoopTrigger functions as the bridge between the new coroutine measureRepeated
+ * implementation and the (soon to be) legacy Java API.
+ *
+ * It allows the vast majority of the benchmark library to be written in coroutines (with very
+ * deliberate suspend calls, generally just for yielding the main thread) and still function within
+ * a runBlocking block inside of `benchmarkState.keepRunning()`
+ *
+ * Eventually, the BenchmarkState api will be deprecated in favor of a Java-friendly variant of
+ * measureRepeated, but this code will remain (ideally without significant change) to support the
+ * BenchmarkState API in the long term.
+ */
+private class SuspendedLoopTrigger : Continuation<Unit> {
+    @JvmField var nextStep: Continuation<Unit>? = null
+    private var next: Int = -1
+    private var done: Boolean = false
+
+    /**
+     * Schedule the loop manager Yields a value of loops to be run by the user of the
+     * SuspendedLoopTrigger.
+     */
+    suspend fun awaitLoops(loopCount: Int) {
+        next = loopCount
+        suspendCoroutineUninterceptedOrReturn { c ->
+            nextStep = c
+            COROUTINE_SUSPENDED
+        }
+    }
+
+    /** Gets the number of loops to run before calling [getNextLoopCount] again */
+    fun getNextLoopCount(): Int {
+        if (done) return 0
+        nextStep!!.resume(Unit)
+        return next
+    }
+
+    override val context: CoroutineContext
+        get() = EmptyCoroutineContext
+
+    override fun resumeWith(result: Result<Unit>) {
+        result.getOrThrow() // just rethrow exception if it is there
+        done = true
+    }
+}
+
+/**
+ * Control object for microbenchmarking in Java.
  *
  * Query a state object with [androidx.benchmark.junit4.BenchmarkRule.getState], and use it to
- * measure a block of Java with [BenchmarkState.keepRunning]:
- * ```java
+ * measure a block of Java with [BenchmarkStateLegacy.keepRunning]:
+ * ```
  * @Rule
  * public BenchmarkRule benchmarkRule = new BenchmarkRule();
  *
@@ -50,589 +118,72 @@
  * }
  * ```
  *
- * @see androidx.benchmark.junit4.BenchmarkRule.getState()
+ * Note that BenchmarkState does not give access to Perfetto traces.
  */
-class BenchmarkState internal constructor(phaseConfig: MicrobenchmarkPhase.Config) {
-
-    /**
-     * Create a BenchmarkState for custom measurement behavior.
-     *
-     * @param warmupCount Number of non-measured warmup iterations to perform, leave null to
-     *   determine automatically
-     * @param repeatCount Number of measurements to perform, leave null for default behavior
-     */
-    @ExperimentalBenchmarkStateApi
-    constructor(
-        @SuppressWarnings("AutoBoxing") // allocations for tests not relevant, not in critical path
-        warmupCount: Int? = null,
-        @SuppressWarnings("AutoBoxing") // allocations for tests not relevant, not in critical path
-        repeatCount: Int? = null
-    ) : this(
-        warmupCount = warmupCount,
-        measurementCount = repeatCount,
-        simplifiedTimingOnlyMode = false
-    )
-
-    /** Constructor used for standard uses of BenchmarkState, e.g. in BenchmarkRule */
+class BenchmarkState
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(testDefinition: TestDefinition, private val config: MicrobenchmarkConfig) {
+    // Secondary explicit constructor allows for internal usage without experimental config opt in
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    constructor(
-        config: MicrobenchmarkConfig? = null
-    ) : this(warmupCount = null, simplifiedTimingOnlyMode = false, config = config)
+    constructor(testDefinition: TestDefinition) : this(testDefinition, MicrobenchmarkConfig())
 
-    internal constructor(
-        warmupCount: Int? = null,
-        measurementCount: Int? = null,
-        simplifiedTimingOnlyMode: Boolean = false,
-        config: MicrobenchmarkConfig? = null
-    ) : this(
-        MicrobenchmarkPhase.Config(
-            dryRunMode = Arguments.dryRunMode,
-            startupMode = Arguments.startupMode,
-            profiler = config?.profiler?.profiler ?: Arguments.profiler,
-            profilerPerfCompareMode = Arguments.profilerPerfCompareEnable,
-            warmupCount = warmupCount,
-            measurementCount = Arguments.iterations ?: measurementCount,
-            simplifiedTimingOnlyMode = simplifiedTimingOnlyMode,
-            metrics =
-                config?.metrics?.toTypedArray()
-                    ?: if (Arguments.cpuEventCounterMask != 0) {
-                        arrayOf(
-                            TimeCapture(),
-                            CpuEventCounterCapture(
-                                MicrobenchmarkPhase.cpuEventCounter,
-                                Arguments.cpuEventCounterMask
-                            )
-                        )
-                    } else {
-                        arrayOf(TimeCapture())
-                    }
+    @JvmField
+    @PublishedApi // Previously used by [BenchmarkState.keepRunningInline()]
+    internal var iterationsRemaining = 0
+
+    /** Ideally we'd call into the top level function, but it's non-suspending */
+    private var internalIter = createSuspendedLoop {
+        // Theoretically we'd ideally call into the top level measureRepeated function, but
+        // that function isn't suspending. Making it suspend would allow this call to perform
+        // tracing, but would significantly complicate the thread management of outer layers (e.g.
+        // carefully scheduling where trace capture start/end happens). As this is compat code, we
+        // don't bother.
+        measureRepeatedImplNoTracing(
+            testDefinition,
+            config = config,
+            loopedMeasurementBlock = { microbenchScope, loops ->
+                scope = microbenchScope
+                awaitLoops(loops)
+            }
         )
-    )
-
-    /**
-     * Set this to true to run a simplified timing loop - no allocation tracking, and no global
-     * state set/reset (such as thread priorities)
-     *
-     * This var is used in one of two cases, either set to true by [ThrottleDetector.measureWorkNs]
-     * when device performance testing for thermal throttling in between benchmarks, or in
-     * correctness tests of this library.
-     *
-     * When set to true, indicates that this BenchmarkState **should not**:
-     * - touch thread priorities
-     * - perform allocation counting (only timing results matter)
-     * - call [ThrottleDetector], since it would infinitely recurse
-     */
-    private val simplifiedTimingOnlyMode = phaseConfig.simplifiedTimingOnlyMode
-
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    var traceUniqueName: String = "benchmark"
-
-    internal var warmupRepeats = 0 // number of warmup repeats that occurred
-
-    /**
-     * Decreasing iteration count used when running a multi-iteration measurement phase Used to
-     * determine when a main measurement stage finishes.
-     */
-    @JvmField // Used by [BenchmarkState.keepRunningInline()]
-    @PublishedApi
-    internal var iterationsRemaining: Int = -1
-
-    @Suppress("NOTHING_TO_INLINE")
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    inline fun getIterationsRemaining() = iterationsRemaining
-
-    /**
-     * Number of iterations in a repeat.
-     *
-     * This value is defined in the json, but is written as maximum iterationsPerRepeat across
-     * phases, since nowadays there can be an arbitrary number of phases.
-     *
-     * This is fully compatible for now since e.g. timing and allocation measurement use the same
-     * value, but we should consider tracking and reporting this differently in the json if this
-     * changes.
-     */
-    @VisibleForTesting internal var iterationsPerRepeat = 1
-
-    private val warmupManager = phaseConfig.warmupManager
-
-    private var paused = false
-
-    /** The total duration of sleep due to thermal throttling. */
-    private var thermalThrottleSleepSeconds: Long = 0
-    private var totalRunTimeStartNs: Long = 0 // System.nanoTime() at start of benchmark.
-    private var totalRunTimeNs: Long = 0 // Total run time of a benchmark.
-
-    private var warmupEstimatedIterationTimeNs: Long = -1L
-
-    private val metricResults = mutableListOf<MetricResult>()
-    private var profilerResult: Profiler.ResultFile? = null
-    private val phases = phaseConfig.generatePhases()
-
-    // tracking current phase state
-    private var phaseIndex = -1
-    private var currentPhase: MicrobenchmarkPhase = phases[0]
-    private var currentMetrics: MetricsContainer = phases[0].metricsContainer
-    private var currentMeasurement = 0
-    private var currentLoopsPerMeasurement = 0
-
-    @SuppressLint("MethodNameUnits")
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    fun getMinTimeNanos(): Double {
-        checkFinished()
-        return metricResults.first { it.name == "timeNs" }.min
     }
 
-    private fun checkFinished() {
-        check(phaseIndex >= 0) { "Attempting to interact with a benchmark that wasn't started!" }
-        check(phaseIndex >= phases.size) {
-            "The benchmark hasn't finished! In Java, use " +
-                "while(BenchmarkState.keepRunning()) to ensure keepRunning() returns " +
-                "false before ending your test. In Kotlin, just use " +
-                "benchmarkRule.measureRepeated {} to avoid the problem."
-        }
-    }
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @JvmField var scope: MicrobenchmarkScope? = null
 
-    /**
-     * Stops the benchmark timer.
-     *
-     * This method can be called only when the timer is running.
-     *
-     * ```
-     * @Test
-     * public void bitmapProcessing() {
-     *     final BenchmarkState state = mBenchmarkRule.getState();
-     *     while (state.keepRunning()) {
-     *         state.pauseTiming();
-     *         // disable timing while constructing test input
-     *         Bitmap input = constructTestBitmap();
-     *         state.resumeTiming();
-     *
-     *         processBitmap(input);
-     *     }
-     * }
-     * ```
-     *
-     * @throws [IllegalStateException] if the benchmark is already paused.
-     * @see resumeTiming
-     */
     fun pauseTiming() {
-        check(!paused) { "Unable to pause the benchmark. The benchmark has already paused." }
-        currentMetrics.capturePaused()
-        paused = true
+        scope!!.pauseMeasurement()
     }
 
-    /**
-     * Resumes the benchmark timer.
-     *
-     * This method can be called only when the timer is stopped.
-     *
-     * ```
-     * @Test
-     * public void bitmapProcessing() {
-     *     final BenchmarkState state = mBenchmarkRule.getState();
-     *     while (state.keepRunning()) {
-     *         state.pauseTiming();
-     *         // disable timing while constructing test input
-     *         Bitmap input = constructTestBitmap();
-     *         state.resumeTiming();
-     *
-     *         processBitmap(input);
-     *     }
-     * }
-     * ```
-     *
-     * @throws [IllegalStateException] if the benchmark is already running.
-     * @see pauseTiming
-     */
     fun resumeTiming() {
-        check(paused) { "Unable to resume the benchmark. The benchmark is already running." }
-        currentMetrics.captureResumed()
-        paused = false
+        scope!!.resumeMeasurement()
     }
 
-    private fun startNextPhase(): Boolean {
-        check(phaseIndex < phases.size)
-
-        if (phaseIndex >= 0) {
-            currentPhase.profiler?.run { inMemoryTrace("profiler.stop()") { stop() } }
-            InMemoryTracing.endSection() // end phase
-            thermalThrottleSleepSeconds += currentPhase.thermalThrottleSleepSeconds
-            if (currentPhase.loopMode.warmupManager == null) {
-                // Save captured metrics except during warmup, where we intentionally discard
-                metricResults.addAll(
-                    currentMetrics.captureFinished(maxIterations = currentLoopsPerMeasurement)
-                )
-            }
-        }
-        phaseIndex++
-        if (phaseIndex == phases.size) {
-            afterBenchmark()
-            return false
-        }
-        currentPhase = phases[phaseIndex]
-        currentMetrics = currentPhase.metricsContainer
-        currentMeasurement = 0
-
-        currentMetrics.captureInit()
-        if (currentPhase.gcBeforePhase) {
-            // Run GC to avoid memory pressure from previous run from affecting this one.
-            // Note, we don't use System.gc() because it doesn't always have consistent behavior
-            Runtime.getRuntime().gc()
-        }
-
-        currentLoopsPerMeasurement =
-            currentPhase.loopMode.getIterations(warmupEstimatedIterationTimeNs)
-
-        iterationsPerRepeat = iterationsPerRepeat.coerceAtLeast(currentLoopsPerMeasurement)
-
-        InMemoryTracing.beginSection(currentPhase.label)
-        val phaseProfilerResult =
-            currentPhase.profiler?.run {
-                val estimatedMethodTraceDurNs =
-                    warmupEstimatedIterationTimeNs * METHOD_TRACING_ESTIMATED_SLOWDOWN_FACTOR
-                if (
-                    this == MethodTracing &&
-                        Looper.myLooper() == Looper.getMainLooper() &&
-                        estimatedMethodTraceDurNs > METHOD_TRACING_MAX_DURATION_NS &&
-                        Arguments.profilerSkipWhenDurationRisksAnr
-                ) {
-                    val expectedDurSec = estimatedMethodTraceDurNs / 1_000_000_000.0
-                    InstrumentationResults.scheduleIdeWarningOnNextReport(
-                        """
-                        Skipping method trace of estimated duration $expectedDurSec sec to avoid ANR
-
-                        To disable this behavior, set instrumentation arg:
-                            androidx.benchmark.profiling.skipWhenDurationRisksAnr = false
-                    """
-                            .trimIndent()
-                    )
-                    null
-                } else {
-                    inMemoryTrace("start profiling") { start(traceUniqueName) }
-                }
-            }
-        if (phaseProfilerResult != null) {
-            require(profilerResult == null) {
-                "ProfileResult already set, only support one profiling phase"
-            }
-            profilerResult = phaseProfilerResult
-        }
-
-        // Warm up the metrics data structure to reduce the impact on the first measurement.
-        currentMetrics.captureStart()
-        currentMetrics.captureStop()
-        currentMetrics.captureInit()
-
-        currentMetrics.captureStart()
-        return true
-    }
-
-    /** @return true if the benchmark should still keep running */
-    private fun onMeasurementComplete(): Boolean {
-        currentMetrics.captureStop()
-        throwIfPaused()
-        currentMeasurement++
-
-        val tryStartNextPhase =
-            currentPhase.loopMode.let {
-                if (it.warmupManager != null) {
-                    // warmup phase
-                    currentMetrics.captureInit()
-                    // Note that warmup is based on repeat time, *not* the timeNs metric, since we
-                    // want
-                    // to account for paused time during warmup (paused work should stabilize too)
-                    val lastMeasuredWarmupValue = currentMetrics.peekSingleRepeatTime()
-                    if (it.warmupManager.onNextIteration(lastMeasuredWarmupValue)) {
-                        warmupEstimatedIterationTimeNs = lastMeasuredWarmupValue
-                        warmupRepeats = currentMeasurement
-                        true
-                    } else {
-                        false
-                    }
-                } else {
-                    currentMeasurement == currentPhase.measurementCount
-                }
-            }
-        return if (tryStartNextPhase) {
-            if (currentPhase.tryEnd()) {
-                startNextPhase()
-            } else {
-                // failed capture (due to thermal throttling), restart profiler and metrics
-                currentPhase.profiler?.apply {
-                    stop()
-                    profilerResult = inMemoryTrace("start profiling") { start(traceUniqueName) }
-                }
-                currentMetrics.captureInit()
-                currentMeasurement = 0
-                true
-            }
-        } else {
-            currentMetrics.captureStart()
-            true
-        }
-    }
-
-    /**
-     * Inline fast-path function for inner benchmark loop.
-     *
-     * Kotlin users should use `BenchmarkRule.measureRepeated`
-     *
-     * This code path uses exclusively @JvmField/const members, so there are no method calls at all
-     * in the inlined loop. On recent Android Platform versions, ART inlines these accessors anyway,
-     * but we want to be sure it's as simple as possible.
-     */
-    @Suppress("NOTHING_TO_INLINE")
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    inline fun keepRunningInline(): Boolean {
-        if (iterationsRemaining > 1) {
-            iterationsRemaining--
-            return true
-        }
-        return keepRunningInternal()
-    }
-
-    /**
-     * Returns true if the benchmark needs more samples - use this as the condition of a while loop.
-     *
-     * ```
-     * while (state.keepRunning()) {
-     *     int[] dest = new int[src.length];
-     *     System.arraycopy(src, 0, dest, 0, src.length);
-     * }
-     * ```
-     */
-    fun keepRunning(): Boolean {
-        if (iterationsRemaining > 1) {
-            iterationsRemaining--
-            return true
-        }
-        return keepRunningInternal()
-    }
-
-    /**
-     * Reimplementation of Kotlin check, which also resets thread priority, since we don't want to
-     * leave a thread with bumped thread priority
-     */
-    private inline fun check(value: Boolean, lazyMessage: () -> String) {
-        if (!value) {
-            cleanupBeforeThrow()
-            throw IllegalStateException(lazyMessage())
-        }
-    }
-
-    /**
-     * Ideally this would only be called when an exception is observed in measureRepeated, but to
-     * account for java callers, we explicitly trigger before throwing as well.
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    fun cleanupBeforeThrow() {
-        if (phaseIndex >= 0 && phaseIndex <= phases.size) {
-            Log.d(TAG, "aborting and cancelling benchmark")
-            // current phase cancelled, complete current phase cleanup (trace event and profiling)
-            InMemoryTracing.endSection()
-            currentPhase.profiler?.run { inMemoryTrace("profiling stop") { stop() } }
-
-            // for safety, set other state to done and do broader cleanup
-            phaseIndex = phases.size
-            afterBenchmark()
-        }
-    }
-
-    /**
-     * Internal loop control for benchmarks - will return true as long as there are more
-     * measurements to perform.
-     *
-     * Actual benchmarks should always go through [keepRunning] or [keepRunningInline], since they
-     * optimize the *Iteration* step to have extremely minimal logic performed.
-     *
-     * The looping behavior is functionally multiple nested loops, e.g.:
-     * - Stage - RUNNING_WARMUP vs RUNNING_TIME
-     * - Measurement - how many times iterations are measured
-     * - Iteration - how many iterations/loops are run between each measurement
-     *
-     * This has the effect of a 3 layer nesting loop structure, but all condensed to a single method
-     * returning true/false to simplify the entry point.
-     *
-     * @return whether the benchmarking system has anything left to do
-     */
     @PublishedApi
     internal fun keepRunningInternal(): Boolean {
-        val shouldKeepRunning =
-            if (phaseIndex == -1) {
-                // Initialize
-                beforeBenchmark()
-                startNextPhase()
-            } else {
-                // Trigger another repeat within current phase
-                onMeasurementComplete()
-            }
-
-        iterationsRemaining = currentLoopsPerMeasurement
-        return shouldKeepRunning
+        iterationsRemaining = internalIter.getNextLoopCount()
+        if (iterationsRemaining > 0) {
+            iterationsRemaining--
+            return true
+        }
+        return false
     }
 
-    private fun beforeBenchmark() {
-        Errors.throwIfError()
-        if (!firstBenchmark && Arguments.startupMode) {
-            throw AssertionError(
-                "Error - multiple benchmarks in startup mode. Only one " +
-                    "benchmark may be run per 'am instrument' call, to ensure result " +
-                    "isolation."
-            )
+    fun keepRunning(): Boolean {
+        if (iterationsRemaining > 0) {
+            iterationsRemaining--
+            return true
         }
-        check(DeviceInfo.artMainlineVersion != DeviceInfo.ART_MAINLINE_VERSION_UNDETECTED_ERROR) {
-            "Unable to detect ART mainline module version to check for interference from method" +
-                " tracing, please see logcat for details, and/or file a bug with logcat."
-        }
-        check(
-            !enableMethodTracingAffectsMeasurementError ||
-                !DeviceInfo.methodTracingAffectsMeasurements ||
-                !MethodTracing.hasBeenUsed
-        ) {
-            "Measurement prevented by method trace - Running on a device/configuration where " +
-                "method tracing affects measurements, and a method trace has been captured " +
-                "- no additional benchmarks can be run without restarting the test suite. Use " +
-                "ProfilerConfig.MethodTracing.affectsMeasurementOnThisDevice to detect affected " +
-                "devices, see its documentation for more info."
-        }
-
-        thermalThrottleSleepSeconds = 0
-
-        if (!simplifiedTimingOnlyMode) {
-            ThrottleDetector.computeThrottleBaselineIfNeeded()
-            ThreadPriority.bumpCurrentThreadPriority()
-        }
-
-        totalRunTimeStartNs = System.nanoTime() // Record this time to find total duration
+        return keepRunningInternal()
     }
 
-    private fun afterBenchmark() {
-        totalRunTimeNs = System.nanoTime() - totalRunTimeStartNs
-
-        if (!simplifiedTimingOnlyMode) {
-            // Don't modify thread priority when checking for thermal throttling, since 'outer'
-            // BenchmarkState owns thread priority
-            ThreadPriority.resetBumpedThread()
-        }
-        warmupManager.logInfo()
-    }
-
-    private fun throwIfPaused() =
-        check(!paused) {
-            "Benchmark loop finished in paused state." +
-                " Call BenchmarkState.resumeTiming() before BenchmarkState.keepRunning()."
-        }
-
-    private fun getTestResult(testName: String, className: String, perfettoTracePath: String?) =
-        BenchmarkData.TestResult(
-            name = testName,
-            className = className,
-            totalRunTimeNs = totalRunTimeNs,
-            metrics = metricResults,
-            warmupIterations = warmupRepeats,
-            repeatIterations = iterationsPerRepeat,
-            thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
-            profilerOutputs =
-                listOfNotNull(
-                    perfettoTracePath?.let {
-                        BenchmarkData.TestResult.ProfilerOutput(
-                            Profiler.ResultFile.ofPerfettoTrace(
-                                label = "Trace",
-                                absolutePath = perfettoTracePath
-                            )
-                        )
-                    },
-                    profilerResult?.let { BenchmarkData.TestResult.ProfilerOutput(it) }
-                )
-        )
-
-    @ExperimentalBenchmarkStateApi
-    fun getMeasurementTimeNs(): List<Double> = metricResults.first { it.name == "timeNs" }.data
-
-    internal fun peekTestResult() =
-        checkFinished().run {
-            getTestResult(testName = "", className = "", perfettoTracePath = null)
-        }
-
-    /**
-     * Acquires a status report bundle
-     *
-     * @param key Run identifier, prepended to bundle properties.
-     * @param reportMetrics True if stats should be included in the output bundle.
-     */
-    internal fun getFullStatusReport(
-        key: String,
-        reportMetrics: Boolean,
-        tracePath: String?
-    ): Bundle {
-        Log.i(TAG, key + metricResults.map { it.getSummary() } + "count=$iterationsPerRepeat")
-        val status = Bundle()
-        if (reportMetrics) {
-            // these 'legacy' CI output metrics are considered output
-            metricResults.forEach { it.putInBundle(status, PREFIX) }
-        }
-        InstrumentationResultScope(status)
-            .reportSummaryToIde(
-                testName = key,
-                measurements =
-                    Measurements(singleMetrics = metricResults, sampledMetrics = emptyList()),
-                profilerResults =
-                    listOfNotNull(
-                        tracePath?.let {
-                            Profiler.ResultFile.ofPerfettoTrace(
-                                label = "Trace",
-                                absolutePath = tracePath
-                            )
-                        },
-                        profilerResult
-                    )
-            )
-        return status
-    }
-
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    fun report(
-        fullClassName: String,
-        simpleClassName: String,
-        methodName: String,
-        perfettoTracePath: String?
-    ) {
-        if (phaseIndex == -1) {
-            return // nothing to report, BenchmarkState wasn't used
-        }
-
-        profilerResult?.convertBeforeSync?.invoke()
-        if (perfettoTracePath != null) {
-            profilerResult?.embedInPerfettoTrace(perfettoTracePath)
-        }
-
-        checkFinished() // this method is triggered externally
-        val fullTestName = "$PREFIX$simpleClassName.$methodName"
-        val bundle =
-            getFullStatusReport(
-                key = fullTestName,
-                reportMetrics = !Arguments.dryRunMode,
-                tracePath = perfettoTracePath
-            )
-        reportBundle(bundle)
-        ResultWriter.appendTestResult(
-            getTestResult(
-                testName = PREFIX + methodName,
-                className = fullClassName,
-                perfettoTracePath = perfettoTracePath
-            )
-        )
-    }
-
+    // Note: Constants left here to avoid churn, but should eventually be moved out to more
+    // appropriate locations
     companion object {
         internal const val TAG = "Benchmark"
 
-        internal const val REPEAT_COUNT_ALLOCATION = 5
-
         /**
-         * Conservative estimate for how much method tracing slows down runtime how much longer will
-         * `methodTrace {x()}` be than `x()`
+         * Conservative estimate for how much method tracing slows down runtime - how much longer
+         * will `methodTrace {x()}` be than `x()` for nontrivial workloads.
          *
          * This is a conservative estimate, better version of this would account for OS/Art version
          *
@@ -649,11 +200,10 @@
         internal const val METHOD_TRACING_MAX_DURATION_NS = 4_000_000_000
 
         internal val DEFAULT_MEASUREMENT_DURATION_NS = TimeUnit.MILLISECONDS.toNanos(100)
+
         internal val SAMPLED_PROFILER_DURATION_NS =
             TimeUnit.SECONDS.toNanos(Arguments.profilerSampleDurationSeconds)
 
-        private var firstBenchmark = true
-
         /**
          * Used to disable error to enable internal correctness tests, which need to use method
          * tracing and can safely ignore measurement accuracy
@@ -662,67 +212,5 @@
          * error functionality doesn't handle changing error states dynamically
          */
         internal var enableMethodTracingAffectsMeasurementError = true
-
-        @RequiresOptIn
-        @Retention(AnnotationRetention.BINARY)
-        @Target(AnnotationTarget.FUNCTION)
-        annotation class ExperimentalExternalReport
-
-        /**
-         * Hooks for benchmarks not using [androidx.benchmark.junit4.BenchmarkRule] to register
-         * results.
-         *
-         * Results are printed to Studio console, and added to the output JSON file.
-         *
-         * @param className Name of class the benchmark runs in
-         * @param testName Name of the benchmark
-         * @param totalRunTimeNs The total run time of the benchmark
-         * @param dataNs List of all measured timing results, in nanoseconds
-         * @param warmupIterations Number of iterations of warmup before measurements started.
-         *   Should be no less than 0.
-         * @param thermalThrottleSleepSeconds Number of seconds benchmark was paused during thermal
-         *   throttling.
-         * @param repeatIterations Number of iterations in between each measurement. Should be no
-         *   less than 1.
-         */
-        @JvmStatic
-        @ExperimentalExternalReport
-        fun reportData(
-            className: String,
-            testName: String,
-            @IntRange(from = 0) totalRunTimeNs: Long,
-            dataNs: List<Long>,
-            @IntRange(from = 0) warmupIterations: Int,
-            @IntRange(from = 0) thermalThrottleSleepSeconds: Long,
-            @IntRange(from = 1) repeatIterations: Int
-        ) {
-            val metricsContainer = MetricsContainer(repeatCount = dataNs.size)
-            dataNs.forEachIndexed { index, value -> metricsContainer.data[index][0] = value }
-            val metrics = metricsContainer.captureFinished(maxIterations = 1)
-            val report =
-                BenchmarkData.TestResult(
-                    className = className,
-                    name = testName,
-                    totalRunTimeNs = totalRunTimeNs,
-                    metrics = metrics,
-                    repeatIterations = repeatIterations,
-                    thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
-                    warmupIterations = warmupIterations,
-                    profilerOutputs = null,
-                )
-            // Report value to Studio console
-            val fullTestName =
-                PREFIX + if (className.isNotEmpty()) "$className.$testName" else testName
-
-            instrumentationReport {
-                reportSummaryToIde(
-                    testName = fullTestName,
-                    measurements = Measurements(metrics, emptyList()),
-                )
-            }
-
-            // Report values to file output
-            ResultWriter.appendTestResult(report)
-        }
     }
 }
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkStateLegacy.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkStateLegacy.kt
new file mode 100644
index 0000000..54021db
--- /dev/null
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkStateLegacy.kt
@@ -0,0 +1,722 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.os.Looper
+import android.util.Log
+import androidx.annotation.IntRange
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
+import androidx.benchmark.Errors.PREFIX
+import androidx.benchmark.InstrumentationResults.instrumentationReport
+import androidx.benchmark.InstrumentationResults.reportBundle
+import androidx.benchmark.json.BenchmarkData
+import java.util.concurrent.TimeUnit
+
+/**
+ * Control object for benchmarking in the code in Java.
+ *
+ * Query a state object with [androidx.benchmark.junit4.BenchmarkRule.getState], and use it to
+ * measure a block of Java with [BenchmarkStateLegacy.keepRunning]:
+ * ```java
+ * @Rule
+ * public BenchmarkRule benchmarkRule = new BenchmarkRule();
+ *
+ * @Test
+ * public void sampleMethod() {
+ *     BenchmarkState state = benchmarkRule.getState();
+ *
+ *     int[] src = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ *     while (state.keepRunning()) {
+ *         int[] dest = new int[src.length];
+ *         System.arraycopy(src, 0, dest, 0, src.length);
+ *     }
+ * }
+ * ```
+ *
+ * @see androidx.benchmark.junit4.BenchmarkRule.getState()
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // temporarily left in place
+class BenchmarkStateLegacy internal constructor(phaseConfig: MicrobenchmarkPhase.Config) {
+
+    /**
+     * Create a BenchmarkState for custom measurement behavior.
+     *
+     * @param warmupCount Number of non-measured warmup iterations to perform, leave null to
+     *   determine automatically
+     * @param repeatCount Number of measurements to perform, leave null for default behavior
+     */
+    @ExperimentalBenchmarkStateApi
+    constructor(
+        @SuppressWarnings("AutoBoxing") // allocations for tests not relevant, not in critical path
+        warmupCount: Int? = null,
+        @SuppressWarnings("AutoBoxing") // allocations for tests not relevant, not in critical path
+        repeatCount: Int? = null
+    ) : this(
+        warmupCount = warmupCount,
+        measurementCount = repeatCount,
+        simplifiedTimingOnlyMode = false
+    )
+
+    /** Constructor used for standard uses of BenchmarkState, e.g. in BenchmarkRule */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    constructor(
+        config: MicrobenchmarkConfig? = null
+    ) : this(warmupCount = null, simplifiedTimingOnlyMode = false, config = config)
+
+    internal constructor(
+        warmupCount: Int? = null,
+        measurementCount: Int? = null,
+        simplifiedTimingOnlyMode: Boolean = false,
+        config: MicrobenchmarkConfig? = null
+    ) : this(
+        MicrobenchmarkPhase.Config(
+            dryRunMode = Arguments.dryRunMode,
+            startupMode = Arguments.startupMode,
+            profiler = config?.profiler?.profiler ?: Arguments.profiler,
+            profilerPerfCompareMode = Arguments.profilerPerfCompareEnable,
+            warmupCount = warmupCount,
+            measurementCount = Arguments.iterations ?: measurementCount,
+            simplifiedTimingOnlyMode = simplifiedTimingOnlyMode,
+            metrics =
+                config?.metrics?.toTypedArray()
+                    ?: if (Arguments.cpuEventCounterMask != 0) {
+                        arrayOf(
+                            TimeCapture(),
+                            CpuEventCounterCapture(
+                                MicrobenchmarkPhase.cpuEventCounter,
+                                Arguments.cpuEventCounterMask
+                            )
+                        )
+                    } else {
+                        arrayOf(TimeCapture())
+                    }
+        )
+    )
+
+    /**
+     * Set this to true to run a simplified timing loop - no allocation tracking, and no global
+     * state set/reset (such as thread priorities)
+     *
+     * This var is used in one of two cases, either set to true by [ThrottleDetector.measureWorkNs]
+     * when device performance testing for thermal throttling in between benchmarks, or in
+     * correctness tests of this library.
+     *
+     * When set to true, indicates that this BenchmarkState **should not**:
+     * - touch thread priorities
+     * - perform allocation counting (only timing results matter)
+     * - call [ThrottleDetector], since it would infinitely recurse
+     */
+    private val simplifiedTimingOnlyMode = phaseConfig.simplifiedTimingOnlyMode
+
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    var traceUniqueName: String = "benchmark"
+
+    internal var warmupRepeats = 0 // number of warmup repeats that occurred
+
+    /**
+     * Decreasing iteration count used when running a multi-iteration measurement phase Used to
+     * determine when a main measurement stage finishes.
+     */
+    @Suppress("ShowingMemberInHiddenClass")
+    @JvmField
+    @PublishedApi // previously used by [BenchmarkState.keepRunningInline()]
+    internal var iterationsRemaining: Int = -1
+
+    @Suppress("NOTHING_TO_INLINE")
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    inline fun getIterationsRemaining() = iterationsRemaining
+
+    /**
+     * Number of iterations in a repeat.
+     *
+     * This value is defined in the json, but is written as maximum iterationsPerRepeat across
+     * phases, since nowadays there can be an arbitrary number of phases.
+     *
+     * This is fully compatible for now since e.g. timing and allocation measurement use the same
+     * value, but we should consider tracking and reporting this differently in the json if this
+     * changes.
+     */
+    @VisibleForTesting internal var iterationsPerRepeat = 1
+
+    private val warmupManager = phaseConfig.warmupManager
+
+    private var paused = false
+
+    /** The total duration of sleep due to thermal throttling. */
+    private var thermalThrottleSleepSeconds: Long = 0
+    private var totalRunTimeStartNs: Long = 0 // System.nanoTime() at start of benchmark.
+    private var totalRunTimeNs: Long = 0 // Total run time of a benchmark.
+
+    private var warmupEstimatedIterationTimeNs: Long = -1L
+
+    private val metricResults = mutableListOf<MetricResult>()
+    private var profilerResult: Profiler.ResultFile? = null
+    private val phases = phaseConfig.generatePhases()
+
+    // tracking current phase state
+    private var phaseIndex = -1
+    private var currentPhase: MicrobenchmarkPhase = phases[0]
+    private var currentMetrics: MetricsContainer = phases[0].metricsContainer
+    private var currentMeasurement = 0
+    private var currentLoopsPerMeasurement = 0
+
+    @SuppressLint("MethodNameUnits")
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    fun getMinTimeNanos(): Double {
+        checkFinished()
+        return metricResults.first { it.name == "timeNs" }.min
+    }
+
+    private fun checkFinished() {
+        check(phaseIndex >= 0) { "Attempting to interact with a benchmark that wasn't started!" }
+        check(phaseIndex >= phases.size) {
+            "The benchmark hasn't finished! In Java, use " +
+                "while(BenchmarkState.keepRunning()) to ensure keepRunning() returns " +
+                "false before ending your test. In Kotlin, just use " +
+                "benchmarkRule.measureRepeated {} to avoid the problem."
+        }
+    }
+
+    /**
+     * Stops the benchmark timer.
+     *
+     * This method can be called only when the timer is running.
+     *
+     * ```
+     * @Test
+     * public void bitmapProcessing() {
+     *     final BenchmarkState state = mBenchmarkRule.getState();
+     *     while (state.keepRunning()) {
+     *         state.pauseTiming();
+     *         // disable timing while constructing test input
+     *         Bitmap input = constructTestBitmap();
+     *         state.resumeTiming();
+     *
+     *         processBitmap(input);
+     *     }
+     * }
+     * ```
+     *
+     * @throws [IllegalStateException] if the benchmark is already paused.
+     * @see resumeTiming
+     */
+    fun pauseTiming() {
+        check(!paused) { "Unable to pause the benchmark. The benchmark has already paused." }
+        currentMetrics.capturePaused()
+        paused = true
+    }
+
+    /**
+     * Resumes the benchmark timer.
+     *
+     * This method can be called only when the timer is stopped.
+     *
+     * ```
+     * @Test
+     * public void bitmapProcessing() {
+     *     final BenchmarkState state = mBenchmarkRule.getState();
+     *     while (state.keepRunning()) {
+     *         state.pauseTiming();
+     *         // disable timing while constructing test input
+     *         Bitmap input = constructTestBitmap();
+     *         state.resumeTiming();
+     *
+     *         processBitmap(input);
+     *     }
+     * }
+     * ```
+     *
+     * @throws [IllegalStateException] if the benchmark is already running.
+     * @see pauseTiming
+     */
+    fun resumeTiming() {
+        check(paused) { "Unable to resume the benchmark. The benchmark is already running." }
+        currentMetrics.captureResumed()
+        paused = false
+    }
+
+    private fun startNextPhase(): Boolean {
+        check(phaseIndex < phases.size)
+
+        if (phaseIndex >= 0) {
+            currentPhase.profiler?.run { inMemoryTrace("profiler.stop()") { stop() } }
+            InMemoryTracing.endSection() // end phase
+            thermalThrottleSleepSeconds += currentPhase.thermalThrottleSleepSeconds
+            if (currentPhase.loopMode.warmupManager == null) {
+                // Save captured metrics except during warmup, where we intentionally discard
+                metricResults.addAll(
+                    currentMetrics.captureFinished(maxIterations = currentLoopsPerMeasurement)
+                )
+            }
+        }
+        phaseIndex++
+        if (phaseIndex == phases.size) {
+            afterBenchmark()
+            return false
+        }
+        currentPhase = phases[phaseIndex]
+        currentMetrics = currentPhase.metricsContainer
+        currentMeasurement = 0
+
+        currentMetrics.captureInit()
+        if (currentPhase.gcBeforePhase) {
+            // Run GC to avoid memory pressure from previous run from affecting this one.
+            // Note, we don't use System.gc() because it doesn't always have consistent behavior
+            Runtime.getRuntime().gc()
+        }
+
+        currentLoopsPerMeasurement =
+            currentPhase.loopMode.getIterations(warmupEstimatedIterationTimeNs)
+
+        iterationsPerRepeat = iterationsPerRepeat.coerceAtLeast(currentLoopsPerMeasurement)
+
+        InMemoryTracing.beginSection(currentPhase.label)
+        val phaseProfilerResult =
+            currentPhase.profiler?.run {
+                val estimatedMethodTraceDurNs =
+                    warmupEstimatedIterationTimeNs * METHOD_TRACING_ESTIMATED_SLOWDOWN_FACTOR
+                if (
+                    this == MethodTracing &&
+                        Looper.myLooper() == Looper.getMainLooper() &&
+                        estimatedMethodTraceDurNs > METHOD_TRACING_MAX_DURATION_NS &&
+                        Arguments.profilerSkipWhenDurationRisksAnr
+                ) {
+                    val expectedDurSec = estimatedMethodTraceDurNs / 1_000_000_000.0
+                    InstrumentationResults.scheduleIdeWarningOnNextReport(
+                        """
+                        Skipping method trace of estimated duration $expectedDurSec sec to avoid ANR
+
+                        To disable this behavior, set instrumentation arg:
+                            androidx.benchmark.profiling.skipWhenDurationRisksAnr = false
+                    """
+                            .trimIndent()
+                    )
+                    null
+                } else {
+                    inMemoryTrace("start profiling") { start(traceUniqueName) }
+                }
+            }
+        if (phaseProfilerResult != null) {
+            require(profilerResult == null) {
+                "ProfileResult already set, only support one profiling phase"
+            }
+            profilerResult = phaseProfilerResult
+        }
+
+        // Warm up the metrics data structure to reduce the impact on the first measurement.
+        currentMetrics.captureStart()
+        currentMetrics.captureStop()
+        currentMetrics.captureInit()
+
+        currentMetrics.captureStart()
+        return true
+    }
+
+    /** @return true if the benchmark should still keep running */
+    private fun onMeasurementComplete(): Boolean {
+        currentMetrics.captureStop()
+        throwIfPaused()
+        currentMeasurement++
+
+        val tryStartNextPhase =
+            currentPhase.loopMode.let {
+                if (it.warmupManager != null) {
+                    // warmup phase
+                    currentMetrics.captureInit()
+                    // Note that warmup is based on repeat time, *not* the timeNs metric, since we
+                    // want
+                    // to account for paused time during warmup (paused work should stabilize too)
+                    val lastMeasuredWarmupValue = currentMetrics.peekSingleRepeatTime()
+                    if (it.warmupManager.onNextIteration(lastMeasuredWarmupValue)) {
+                        warmupEstimatedIterationTimeNs = lastMeasuredWarmupValue
+                        warmupRepeats = currentMeasurement
+                        true
+                    } else {
+                        false
+                    }
+                } else {
+                    currentMeasurement == currentPhase.measurementCount
+                }
+            }
+        return if (tryStartNextPhase) {
+            if (currentPhase.tryEnd()) {
+                startNextPhase()
+            } else {
+                // failed capture (due to thermal throttling), restart profiler and metrics
+                currentPhase.profiler?.apply {
+                    stop()
+                    profilerResult = inMemoryTrace("start profiling") { start(traceUniqueName) }
+                }
+                currentMetrics.captureInit()
+                currentMeasurement = 0
+                true
+            }
+        } else {
+            currentMetrics.captureStart()
+            true
+        }
+    }
+
+    /**
+     * Inline fast-path function for inner benchmark loop.
+     *
+     * Kotlin users should use `BenchmarkRule.measureRepeated`
+     *
+     * This code path uses exclusively @JvmField/const members, so there are no method calls at all
+     * in the inlined loop. On recent Android Platform versions, ART inlines these accessors anyway,
+     * but we want to be sure it's as simple as possible.
+     */
+    @Suppress("NOTHING_TO_INLINE")
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    inline fun keepRunningInline(): Boolean {
+        if (iterationsRemaining > 1) {
+            iterationsRemaining--
+            return true
+        }
+        return keepRunningInternal()
+    }
+
+    /**
+     * Returns true if the benchmark needs more samples - use this as the condition of a while loop.
+     *
+     * ```
+     * while (state.keepRunning()) {
+     *     int[] dest = new int[src.length];
+     *     System.arraycopy(src, 0, dest, 0, src.length);
+     * }
+     * ```
+     */
+    fun keepRunning(): Boolean {
+        if (iterationsRemaining > 1) {
+            iterationsRemaining--
+            return true
+        }
+        return keepRunningInternal()
+    }
+
+    /**
+     * Reimplementation of Kotlin check, which also resets thread priority, since we don't want to
+     * leave a thread with bumped thread priority
+     */
+    private inline fun check(value: Boolean, lazyMessage: () -> String) {
+        if (!value) {
+            cleanupBeforeThrow()
+            throw IllegalStateException(lazyMessage())
+        }
+    }
+
+    /**
+     * Ideally this would only be called when an exception is observed in measureRepeated, but to
+     * account for java callers, we explicitly trigger before throwing as well.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    fun cleanupBeforeThrow() {
+        if (phaseIndex >= 0 && phaseIndex <= phases.size) {
+            Log.d(TAG, "aborting and cancelling benchmark")
+            // current phase cancelled, complete current phase cleanup (trace event and profiling)
+            InMemoryTracing.endSection()
+            currentPhase.profiler?.run { inMemoryTrace("profiling stop") { stop() } }
+
+            // for safety, set other state to done and do broader cleanup
+            phaseIndex = phases.size
+            afterBenchmark()
+        }
+    }
+
+    /**
+     * Internal loop control for benchmarks - will return true as long as there are more
+     * measurements to perform.
+     *
+     * Actual benchmarks should always go through [keepRunning] or [keepRunningInline], since they
+     * optimize the *Iteration* step to have extremely minimal logic performed.
+     *
+     * The looping behavior is functionally multiple nested loops, e.g.:
+     * - Stage - RUNNING_WARMUP vs RUNNING_TIME
+     * - Measurement - how many times iterations are measured
+     * - Iteration - how many iterations/loops are run between each measurement
+     *
+     * This has the effect of a 3 layer nesting loop structure, but all condensed to a single method
+     * returning true/false to simplify the entry point.
+     *
+     * @return whether the benchmarking system has anything left to do
+     */
+    @Suppress("ShowingMemberInHiddenClass")
+    @PublishedApi
+    internal fun keepRunningInternal(): Boolean {
+        val shouldKeepRunning =
+            if (phaseIndex == -1) {
+                // Initialize
+                beforeBenchmark()
+                startNextPhase()
+            } else {
+                // Trigger another repeat within current phase
+                onMeasurementComplete()
+            }
+
+        iterationsRemaining = currentLoopsPerMeasurement
+        return shouldKeepRunning
+    }
+
+    private fun beforeBenchmark() {
+        Errors.throwIfError()
+        if (!firstBenchmark && Arguments.startupMode) {
+            throw AssertionError(
+                "Error - multiple benchmarks in startup mode. Only one " +
+                    "benchmark may be run per 'am instrument' call, to ensure result " +
+                    "isolation."
+            )
+        }
+        check(DeviceInfo.artMainlineVersion != DeviceInfo.ART_MAINLINE_VERSION_UNDETECTED_ERROR) {
+            "Unable to detect ART mainline module version to check for interference from method" +
+                " tracing, please see logcat for details, and/or file a bug with logcat."
+        }
+        check(
+            !BenchmarkState.enableMethodTracingAffectsMeasurementError ||
+                !DeviceInfo.methodTracingAffectsMeasurements ||
+                !MethodTracing.hasBeenUsed
+        ) {
+            "Measurement prevented by method trace - Running on a device/configuration where " +
+                "method tracing affects measurements, and a method trace has been captured " +
+                "- no additional benchmarks can be run without restarting the test suite. Use " +
+                "ProfilerConfig.MethodTracing.affectsMeasurementOnThisDevice to detect affected " +
+                "devices, see its documentation for more info."
+        }
+
+        thermalThrottleSleepSeconds = 0
+
+        if (!simplifiedTimingOnlyMode) {
+            ThrottleDetector.computeThrottleBaselineIfNeeded()
+            ThreadPriority.bumpCurrentThreadPriority()
+        }
+
+        totalRunTimeStartNs = System.nanoTime() // Record this time to find total duration
+    }
+
+    private fun afterBenchmark() {
+        totalRunTimeNs = System.nanoTime() - totalRunTimeStartNs
+
+        if (!simplifiedTimingOnlyMode) {
+            // Don't modify thread priority when checking for thermal throttling, since 'outer'
+            // BenchmarkState owns thread priority
+            ThreadPriority.resetBumpedThread()
+        }
+        warmupManager.logInfo()
+    }
+
+    private fun throwIfPaused() =
+        check(!paused) {
+            "Benchmark loop finished in paused state." +
+                " Call BenchmarkState.resumeTiming() before BenchmarkState.keepRunning()."
+        }
+
+    private fun getTestResult(testName: String, className: String, perfettoTracePath: String?) =
+        BenchmarkData.TestResult(
+            name = testName,
+            className = className,
+            totalRunTimeNs = totalRunTimeNs,
+            metrics = metricResults,
+            warmupIterations = warmupRepeats,
+            repeatIterations = iterationsPerRepeat,
+            thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
+            profilerOutputs =
+                listOfNotNull(
+                    perfettoTracePath?.let {
+                        BenchmarkData.TestResult.ProfilerOutput(
+                            Profiler.ResultFile.ofPerfettoTrace(
+                                label = "Trace",
+                                absolutePath = perfettoTracePath
+                            )
+                        )
+                    },
+                    profilerResult?.let { BenchmarkData.TestResult.ProfilerOutput(it) }
+                )
+        )
+
+    @ExperimentalBenchmarkStateApi
+    fun getMeasurementTimeNs(): List<Double> = metricResults.first { it.name == "timeNs" }.data
+
+    internal fun peekTestResult() =
+        checkFinished().run {
+            getTestResult(testName = "", className = "", perfettoTracePath = null)
+        }
+
+    /**
+     * Acquires a status report bundle
+     *
+     * @param key Run identifier, prepended to bundle properties.
+     * @param reportMetrics True if stats should be included in the output bundle.
+     */
+    internal fun getFullStatusReport(
+        key: String,
+        reportMetrics: Boolean,
+        tracePath: String?
+    ): Bundle {
+        Log.i(TAG, key + metricResults.map { it.getSummary() } + "count=$iterationsPerRepeat")
+        val status = Bundle()
+        if (reportMetrics) {
+            // these 'legacy' CI output metrics are considered output
+            metricResults.forEach { it.putInBundle(status, PREFIX) }
+        }
+        InstrumentationResultScope(status)
+            .reportSummaryToIde(
+                testName = key,
+                measurements =
+                    Measurements(singleMetrics = metricResults, sampledMetrics = emptyList()),
+                profilerResults =
+                    listOfNotNull(
+                        tracePath?.let {
+                            Profiler.ResultFile.ofPerfettoTrace(
+                                label = "Trace",
+                                absolutePath = tracePath
+                            )
+                        },
+                        profilerResult
+                    )
+            )
+        return status
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    fun report(
+        fullClassName: String,
+        simpleClassName: String,
+        methodName: String,
+        perfettoTracePath: String?
+    ) {
+        if (phaseIndex == -1) {
+            return // nothing to report, BenchmarkState wasn't used
+        }
+
+        profilerResult?.convertBeforeSync?.invoke()
+        if (perfettoTracePath != null) {
+            profilerResult?.embedInPerfettoTrace(perfettoTracePath)
+        }
+
+        checkFinished() // this method is triggered externally
+        val fullTestName = "$PREFIX$simpleClassName.$methodName"
+        val bundle =
+            getFullStatusReport(
+                key = fullTestName,
+                reportMetrics = !Arguments.dryRunMode,
+                tracePath = perfettoTracePath
+            )
+        reportBundle(bundle)
+        ResultWriter.appendTestResult(
+            getTestResult(
+                testName = PREFIX + methodName,
+                className = fullClassName,
+                perfettoTracePath = perfettoTracePath
+            )
+        )
+    }
+
+    companion object {
+        internal const val TAG = "Benchmark"
+
+        internal const val REPEAT_COUNT_ALLOCATION = 5
+
+        /**
+         * Conservative estimate for how much method tracing slows down runtime how much longer will
+         * `methodTrace {x()}` be than `x()`
+         *
+         * This is a conservative estimate, better version of this would account for OS/Art version
+         *
+         * Value derived from observed numbers on bramble API 31 (600-800x slowdown)
+         */
+        internal const val METHOD_TRACING_ESTIMATED_SLOWDOWN_FACTOR = 1000
+
+        /**
+         * Maximum duration to trace on main thread to avoid ANRs
+         *
+         * In practice, other types of tracing can be equally dangerous for ANRs, but method tracing
+         * is the default tracing mode.
+         */
+        internal const val METHOD_TRACING_MAX_DURATION_NS = 4_000_000_000
+
+        internal val DEFAULT_MEASUREMENT_DURATION_NS = TimeUnit.MILLISECONDS.toNanos(100)
+        internal val SAMPLED_PROFILER_DURATION_NS =
+            TimeUnit.SECONDS.toNanos(Arguments.profilerSampleDurationSeconds)
+
+        private var firstBenchmark = true
+
+        @RequiresOptIn
+        @Retention(AnnotationRetention.BINARY)
+        @Target(AnnotationTarget.FUNCTION)
+        annotation class ExperimentalExternalReport
+
+        /**
+         * Hooks for benchmarks not using [androidx.benchmark.junit4.BenchmarkRule] to register
+         * results.
+         *
+         * Results are printed to Studio console, and added to the output JSON file.
+         *
+         * @param className Name of class the benchmark runs in
+         * @param testName Name of the benchmark
+         * @param totalRunTimeNs The total run time of the benchmark
+         * @param dataNs List of all measured timing results, in nanoseconds
+         * @param warmupIterations Number of iterations of warmup before measurements started.
+         *   Should be no less than 0.
+         * @param thermalThrottleSleepSeconds Number of seconds benchmark was paused during thermal
+         *   throttling.
+         * @param repeatIterations Number of iterations in between each measurement. Should be no
+         *   less than 1.
+         */
+        @JvmStatic
+        @ExperimentalExternalReport
+        fun reportData(
+            className: String,
+            testName: String,
+            @IntRange(from = 0) totalRunTimeNs: Long,
+            dataNs: List<Long>,
+            @IntRange(from = 0) warmupIterations: Int,
+            @IntRange(from = 0) thermalThrottleSleepSeconds: Long,
+            @IntRange(from = 1) repeatIterations: Int
+        ) {
+            val metricsContainer = MetricsContainer(repeatCount = dataNs.size)
+            dataNs.forEachIndexed { index, value -> metricsContainer.data[index][0] = value }
+            val metrics = metricsContainer.captureFinished(maxIterations = 1)
+            val report =
+                BenchmarkData.TestResult(
+                    className = className,
+                    name = testName,
+                    totalRunTimeNs = totalRunTimeNs,
+                    metrics = metrics,
+                    repeatIterations = repeatIterations,
+                    thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
+                    warmupIterations = warmupIterations,
+                    profilerOutputs = null,
+                )
+            // Report value to Studio console
+            val fullTestName =
+                PREFIX + if (className.isNotEmpty()) "$className.$testName" else testName
+
+            instrumentationReport {
+                reportSummaryToIde(
+                    testName = fullTestName,
+                    measurements = Measurements(metrics, emptyList()),
+                )
+            }
+
+            // Report values to file output
+            ResultWriter.appendTestResult(report)
+        }
+    }
+}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Microbenchmark.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Microbenchmark.kt
new file mode 100644
index 0000000..0fa13ee
--- /dev/null
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Microbenchmark.kt
@@ -0,0 +1,466 @@
+/*
+ * 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.benchmark
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.benchmark.BenchmarkState.Companion.enableMethodTracingAffectsMeasurementError
+import androidx.benchmark.perfetto.PerfettoCapture
+import androidx.benchmark.perfetto.PerfettoCaptureWrapper
+import androidx.benchmark.perfetto.PerfettoConfig
+import androidx.benchmark.perfetto.UiState
+import androidx.benchmark.perfetto.appendUiState
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.tracing.Trace
+import androidx.tracing.trace
+import java.io.File
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+open class MicrobenchmarkScope(internal val state: MicrobenchmarkRunningState) {
+    inline fun <T> runWithMeasurementDisabled(block: () -> T): T {
+        pauseMeasurement()
+        // Note: we only bother with tracing for the runWithTimingDisabled function for
+        // Kotlin callers, as it's more difficult to corrupt the trace with incorrectly
+        // paired BenchmarkState pause/resume calls
+        val ret: T =
+            try {
+                // TODO: use `trace() {}` instead of this manual try/finally,
+                //  once the block parameter is marked crossinline.
+                Trace.beginSection("runWithTimingDisabled")
+                block()
+            } finally {
+                Trace.endSection()
+            }
+        resumeMeasurement()
+        return ret
+    }
+
+    /**
+     * Resume measurement after a call to [pauseMeasurement].
+     *
+     * Kotlin callers should generally instead use [runWithTimingDisabled].
+     */
+    fun pauseMeasurement() {
+        state.pauseMeasurement()
+    }
+
+    /**
+     * Resume measurement after a call to [pauseMeasurement]
+     *
+     * Kotlin callers should generally instead use [runWithTimingDisabled].
+     */
+    fun resumeMeasurement() {
+        state.resumeMeasurement()
+    }
+}
+
+/**
+ * State carried across multiple phases, including metric and output files
+ *
+ * This is maintained as a state object rather than return objects from each phase to avoid
+ * allocation
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class MicrobenchmarkRunningState
+internal constructor(metrics: MetricsContainer, val yieldThreadPeriodically: Boolean) {
+    internal var warmupEstimatedIterationTimeNs: Long = 0
+    internal var warmupIterations: Int = 0
+    internal var totalThermalThrottleSleepSeconds: Long = 0
+    internal var maxIterationsPerRepeat = 0
+    internal var metrics: MetricsContainer = metrics
+    internal var metricResults = mutableListOf<MetricResult>()
+    internal var profilerResults = mutableListOf<Profiler.ResultFile>()
+    internal var paused = false
+
+    internal var initialTimeNs: Long = 0
+    internal var softDeadlineNs: Long = 0
+    internal var hardDeadlineNs: Long = 0
+
+    fun pauseMeasurement() {
+        check(!paused) { "Unable to pause the benchmark. The benchmark has already paused." }
+        metrics.capturePaused()
+        paused = true
+    }
+
+    fun resumeMeasurement() {
+        check(paused) { "Unable to resume the benchmark. The benchmark is already running." }
+        metrics.captureResumed()
+        paused = false
+    }
+
+    fun beginTaskTrace() {
+        if (yieldThreadPeriodically) {
+            Trace.beginSection("benchmark task")
+            initialTimeNs = System.nanoTime()
+            // we try to stop next measurement after soft deadline...
+            softDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(2)
+            // ... and throw if took longer than hard deadline
+            hardDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(10)
+        }
+    }
+
+    fun endTaskTrace() {
+        if (yieldThreadPeriodically) {
+            Trace.endSection()
+        }
+    }
+
+    internal suspend inline fun yieldThreadIfDeadlinePassed() {
+        if (yieldThreadPeriodically) {
+            val timeNs = System.nanoTime()
+            if (timeNs >= softDeadlineNs) {
+
+                if (timeNs > hardDeadlineNs && Arguments.measureRepeatedOnMainThrowOnDeadline) {
+                    val overrunInSec = (timeNs - hardDeadlineNs) / 1_000_000_000.0
+                    // note - we throw without cancelling task trace, since outer layer handles that
+                    throw IllegalStateException(
+                        "Benchmark loop overran hard time limit by $overrunInSec seconds"
+                    )
+                }
+
+                // pause and resume task trace around yield
+                endTaskTrace()
+                yield()
+                beginTaskTrace()
+            }
+        }
+    }
+}
+
+private var firstBenchmark = true
+
+private fun checkForErrors() {
+    Errors.throwIfError()
+    if (!firstBenchmark && Arguments.startupMode) {
+        throw AssertionError(
+            "Error - multiple benchmarks in startup mode. Only one " +
+                "benchmark may be run per 'am instrument' call, to ensure result " +
+                "isolation."
+        )
+    }
+    check(DeviceInfo.artMainlineVersion != DeviceInfo.ART_MAINLINE_VERSION_UNDETECTED_ERROR) {
+        "Unable to detect ART mainline module version to check for interference from method" +
+            " tracing, please see logcat for details, and/or file a bug with logcat."
+    }
+    check(
+        !enableMethodTracingAffectsMeasurementError ||
+            !DeviceInfo.methodTracingAffectsMeasurements ||
+            !MethodTracing.hasBeenUsed
+    ) {
+        "Measurement prevented by method trace - Running on a device/configuration where " +
+            "method tracing affects measurements, and a method trace has been captured " +
+            "- no additional benchmarks can be run without restarting the test suite. Use " +
+            "ProfilerConfig.MethodTracing.affectsMeasurementOnThisDevice to detect affected " +
+            "devices, see its documentation for more info."
+    }
+}
+
+internal typealias LoopedMeasurementBlock = suspend (MicrobenchmarkScope, Int) -> Unit
+
+internal typealias ScopeFactory = (MicrobenchmarkRunningState) -> MicrobenchmarkScope
+
+private fun <T> runBlockingOverrideMain(
+    runOnMainDispatcher: Boolean,
+    block: suspend CoroutineScope.() -> T
+): T {
+    return if (runOnMainDispatcher) {
+        runBlocking(Dispatchers.Main, block)
+    } else {
+        runBlocking { block(this) }
+    }
+}
+
+internal fun captureMicroPerfettoTrace(
+    definition: TestDefinition,
+    config: MicrobenchmarkConfig?,
+    block: () -> Unit
+): String? =
+    PerfettoCaptureWrapper()
+        .record(
+            fileLabel = definition.traceUniqueName,
+            config =
+                PerfettoConfig.Benchmark(
+                    appTagPackages =
+                        if (config?.traceAppTagEnabled == true) {
+                            listOf(InstrumentationRegistry.getInstrumentation().context.packageName)
+                        } else {
+                            emptyList()
+                        },
+                    useStackSamplingConfig = false
+                ),
+            // TODO(290918736): add support for Perfetto SDK Tracing in
+            //  Microbenchmark in other cases, outside of MicrobenchmarkConfig
+            perfettoSdkConfig =
+                if (
+                    config?.perfettoSdkTracingEnabled == true &&
+                        Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                ) {
+                    PerfettoCapture.PerfettoSdkConfig(
+                        InstrumentationRegistry.getInstrumentation().context.packageName,
+                        PerfettoCapture.PerfettoSdkConfig.InitialProcessState.Alive
+                    )
+                } else {
+                    null
+                },
+
+            // Optimize throughput in dryRunMode, since trace isn't useful, and extremely
+            //   expensive on some emulators. Could alternately use UserspaceTracing if
+            // desired
+            // Additionally, skip on misconfigured devices to still enable benchmarking.
+            enableTracing = !Arguments.dryRunMode && !DeviceInfo.misconfiguredForTracing,
+            inMemoryTracingLabel = "Microbenchmark",
+            block = block
+        )
+
+/**
+ * Core engine of microbenchmark, used in one of three ways:
+ * 1. [measureRepeatedImplWithTracing] - standard tracing microbenchmark
+ * 2. [measureRepeatedImplNoTracing] - legacy, non-tracing, suspending functionality backing
+ *    [BenchmarkState] compatibility
+ * 3. [measureRepeatedCheckNanosReentrant] - microbenchmark which avoids modifying global state,
+ *    which runs *within* other variants to check for thermal throttling
+ */
+internal class Microbenchmark(
+    private val definition: TestDefinition,
+    private val phaseConfig: MicrobenchmarkPhase.Config,
+    private val yieldThreadPeriodically: Boolean,
+    private val scopeFactory: ScopeFactory,
+    private val loopedMeasurementBlock: LoopedMeasurementBlock
+) {
+    constructor(
+        definition: TestDefinition,
+        config: MicrobenchmarkConfig,
+        simplifiedTimingOnlyMode: Boolean,
+        yieldThreadPeriodically: Boolean,
+        scopeFactory: ScopeFactory = { runningState -> MicrobenchmarkScope(runningState) },
+        loopedMeasurementBlock: LoopedMeasurementBlock
+    ) : this(
+        definition = definition,
+        phaseConfig = MicrobenchmarkPhase.Config(config, simplifiedTimingOnlyMode),
+        yieldThreadPeriodically = yieldThreadPeriodically,
+        scopeFactory = scopeFactory,
+        loopedMeasurementBlock = loopedMeasurementBlock
+    )
+
+    private var startTimeNs = System.nanoTime()
+
+    init {
+        if (!phaseConfig.simplifiedTimingOnlyMode) {
+            Log.d(TAG, "-- Running ${definition.fullNameUnsanitized} --")
+            checkForErrors()
+        }
+    }
+
+    private val phases = phaseConfig.generatePhases()
+    private val state =
+        MicrobenchmarkRunningState(phases[0].metricsContainer, yieldThreadPeriodically)
+    private val scope = scopeFactory(state)
+
+    suspend fun executePhases() {
+        state.beginTaskTrace()
+        try {
+            if (!phaseConfig.simplifiedTimingOnlyMode) {
+                ThrottleDetector.computeThrottleBaselineIfNeeded()
+                ThreadPriority.bumpCurrentThreadPriority()
+            }
+            firstBenchmark = false
+            phases.forEach {
+                it.execute(
+                    traceUniqueName = definition.traceUniqueName,
+                    scope = scope,
+                    state = state,
+                    loopedMeasurementBlock = loopedMeasurementBlock
+                )
+            }
+        } finally {
+            if (!phaseConfig.simplifiedTimingOnlyMode) {
+                // Don't modify thread priority in simplified timing mode, since 'outer'
+                // measureRepeated owns thread priority
+                ThreadPriority.resetBumpedThread()
+            }
+            phaseConfig.warmupManager.logInfo()
+            state.endTaskTrace()
+        }
+    }
+
+    fun output(perfettoTracePath: String?): MicrobenchmarkOutput {
+        Log.i(
+            BenchmarkState.TAG,
+            definition.outputTestName +
+                state.metricResults.map { it.getSummary() } +
+                "count=${state.maxIterationsPerRepeat}"
+        )
+        return MicrobenchmarkOutput(
+                definition = definition,
+                metricResults = state.metricResults,
+                profilerResults = processProfilerResults(perfettoTracePath),
+                totalRunTimeNs = System.nanoTime() - startTimeNs,
+                warmupIterations = state.warmupIterations,
+                repeatIterations = state.maxIterationsPerRepeat,
+                thermalThrottleSleepSeconds = state.totalThermalThrottleSleepSeconds,
+                reportMetricsInBundle = !Arguments.dryRunMode
+            )
+            .apply {
+                InstrumentationResults.reportBundle(createBundle())
+                ResultWriter.appendTestResult(createJsonTestResult())
+            }
+    }
+
+    fun getMinTimeNanos(): Double {
+        return state.metricResults.first { it.name == "timeNs" }.min
+    }
+
+    private fun processProfilerResults(perfettoTracePath: String?): List<Profiler.ResultFile> {
+        // prepare profiling result files
+        perfettoTracePath?.apply {
+            // trace completed, and copied into shell writeable dir
+            val file = File(this)
+            file.appendUiState(
+                UiState(
+                    timelineStart = null,
+                    timelineEnd = null,
+                    highlightPackage =
+                        InstrumentationRegistry.getInstrumentation().context.packageName
+                )
+            )
+        }
+        state.profilerResults.forEach {
+            it.convertBeforeSync?.invoke()
+            if (perfettoTracePath != null) {
+                it.embedInPerfettoTrace(perfettoTracePath)
+            }
+        }
+        val profilerResults =
+            listOfNotNull(
+                perfettoTracePath?.let {
+                    Profiler.ResultFile.ofPerfettoTrace(label = "Trace", absolutePath = it)
+                }
+            ) + state.profilerResults
+        return profilerResults
+    }
+
+    companion object {
+        internal const val TAG = "Benchmark"
+    }
+}
+
+internal inline fun measureRepeatedCheckNanosReentrant(
+    crossinline measureBlock: MicrobenchmarkScope.() -> Unit
+): Double {
+    return Microbenchmark(
+            TestDefinition(
+                fullClassName = "ThrottleDetector",
+                simpleClassName = "ThrottleDetector",
+                methodName = "checkThrottle"
+            ),
+            config = MicrobenchmarkConfig(),
+            simplifiedTimingOnlyMode = true,
+            yieldThreadPeriodically = false,
+            loopedMeasurementBlock = { scope, iterations ->
+                var remainingIterations = iterations
+                do {
+                    measureBlock.invoke(scope)
+                    remainingIterations--
+                } while (remainingIterations > 0)
+            }
+        )
+        .run {
+            runBlocking { executePhases() }
+            getMinTimeNanos()
+        }
+}
+
+/**
+ * Limited version of [measureRepeatedImplWithTracing] which doesn't capture a trace, and doesn't
+ * support posting work to main thread.
+ */
+internal suspend fun measureRepeatedImplNoTracing(
+    definition: TestDefinition,
+    config: MicrobenchmarkConfig,
+    loopedMeasurementBlock: LoopedMeasurementBlock
+) {
+    Microbenchmark(
+            definition = definition,
+            config = config,
+            simplifiedTimingOnlyMode = false,
+            yieldThreadPeriodically = false,
+            loopedMeasurementBlock = loopedMeasurementBlock
+        )
+        .apply {
+            executePhases()
+            output(perfettoTracePath = null)
+        }
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun measureRepeatedImplWithTracing(
+    definition: TestDefinition,
+    config: MicrobenchmarkConfig?,
+    postToMainThread: Boolean,
+    scopeFactory: ScopeFactory = { runningState -> MicrobenchmarkScope(runningState) },
+    loopedMeasurementBlock: LoopedMeasurementBlock
+) {
+    val microbenchmark =
+        Microbenchmark(
+            definition = definition,
+            config = config ?: MicrobenchmarkConfig(),
+            simplifiedTimingOnlyMode = false,
+            yieldThreadPeriodically = postToMainThread,
+            scopeFactory = scopeFactory,
+            loopedMeasurementBlock = loopedMeasurementBlock
+        )
+    val perfettoTracePath =
+        captureMicroPerfettoTrace(definition, config) {
+            trace(definition.fullNameUnsanitized) {
+                runBlockingOverrideMain(runOnMainDispatcher = postToMainThread) {
+                    microbenchmark.executePhases()
+                }
+            }
+        }
+    microbenchmark.output(perfettoTracePath)
+}
+
+/**
+ * Top level entry point for capturing a microbenchmark with a trace.
+ *
+ * Eventually this method (or one like it) should be public, and also expose a results object
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+inline fun measureRepeated(
+    definition: TestDefinition,
+    config: MicrobenchmarkConfig? = null,
+    crossinline measureBlock: MicrobenchmarkScope.() -> Unit
+) {
+    measureRepeatedImplWithTracing(
+        postToMainThread = false,
+        definition = definition,
+        config = config,
+        loopedMeasurementBlock = { scope, iterations ->
+            var remainingIterations = iterations
+            do {
+                measureBlock.invoke(scope)
+                remainingIterations--
+            } while (remainingIterations > 0)
+        }
+    )
+}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkConfig.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkConfig.kt
index 58ec6af..102de5e 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkConfig.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkConfig.kt
@@ -29,7 +29,18 @@
      *
      * Defaults to [TimeCapture].
      */
-    val metrics: List<MetricCapture> = listOf(TimeCapture()),
+    val metrics: List<MetricCapture> =
+        if (Arguments.cpuEventCounterMask != 0) {
+            listOf(
+                TimeCapture(),
+                CpuEventCounterCapture(
+                    MicrobenchmarkPhase.cpuEventCounter,
+                    Arguments.cpuEventCounterMask
+                )
+            )
+        } else {
+            listOf(TimeCapture())
+        },
 
     /**
      * Set to true to enable capture of `trace("foo") {}` blocks in the output Perfetto trace.
@@ -52,4 +63,10 @@
 
     /** Optional profiler to be used after the primary timing phase. */
     val profiler: ProfilerConfig? = null,
+    @Suppress("AutoBoxing") // null is distinct, and boxing cost is trivial (off critical path)
+    @get:Suppress("AutoBoxing") // null is distinct, and boxing cost is trivial (off critical path)
+    val warmupCount: Int? = null,
+    @Suppress("AutoBoxing") // null is distinct, and boxing cost is trivial (off critical path)
+    @get:Suppress("AutoBoxing") // null is distinct, and boxing cost is trivial (off critical path)
+    val measurementCount: Int? = null
 )
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkOutput.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkOutput.kt
new file mode 100644
index 0000000..a3fab45
--- /dev/null
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkOutput.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark
+
+import android.os.Bundle
+import androidx.benchmark.Errors.PREFIX
+import androidx.benchmark.json.BenchmarkData
+
+internal class MicrobenchmarkOutput(
+    val definition: TestDefinition,
+    val metricResults: List<MetricResult>,
+    val profilerResults: List<Profiler.ResultFile>,
+    val totalRunTimeNs: Long,
+    val warmupIterations: Int,
+    val repeatIterations: Int,
+    val thermalThrottleSleepSeconds: Long,
+    val reportMetricsInBundle: Boolean
+) {
+    fun createBundle() =
+        Bundle().apply {
+            if (reportMetricsInBundle) {
+                // these 'legacy' CI output metrics are considered output
+                metricResults.forEach { it.putInBundle(this, PREFIX) }
+            }
+            InstrumentationResultScope(this)
+                .reportSummaryToIde(
+                    testName = definition.outputTestName,
+                    measurements =
+                        Measurements(singleMetrics = metricResults, sampledMetrics = emptyList()),
+                    profilerResults = profilerResults
+                )
+        }
+
+    fun createJsonTestResult() =
+        BenchmarkData.TestResult(
+            name = definition.outputMethodName,
+            className = definition.fullClassName,
+            totalRunTimeNs = totalRunTimeNs,
+            metrics = metricResults,
+            warmupIterations = warmupIterations,
+            repeatIterations = repeatIterations,
+            thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
+            profilerOutputs = profilerResults.map { BenchmarkData.TestResult.ProfilerOutput(it) }
+        )
+}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkPhase.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkPhase.kt
index 96d686e..8370a6e 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkPhase.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MicrobenchmarkPhase.kt
@@ -4,6 +4,7 @@
 import android.util.Log
 import androidx.benchmark.CpuEventCounter.Event
 import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.delay
 
 internal class MicrobenchmarkPhase(
     val label: String,
@@ -60,6 +61,109 @@
             else -> false
         }
 
+    internal suspend inline fun execute(
+        traceUniqueName: String,
+        scope: MicrobenchmarkScope,
+        state: MicrobenchmarkRunningState,
+        loopedMeasurementBlock: LoopedMeasurementBlock
+    ) {
+        var thermalThrottleSleepsRemaining = thermalThrottleSleepsMax
+        val loopsPerMeasurement = loopMode.getIterations(state.warmupEstimatedIterationTimeNs)
+        state.maxIterationsPerRepeat =
+            state.maxIterationsPerRepeat.coerceAtLeast(loopsPerMeasurement)
+
+        var phaseProfilerResult: Profiler.ResultFile?
+        try {
+            InMemoryTracing.beginSection(label)
+            if (gcBeforePhase) {
+                // Run GC to avoid memory pressure from previous run from affecting this one.
+                // Note, we don't use System.gc() because it doesn't always have consistent behavior
+                Runtime.getRuntime().gc()
+            }
+            while (true) { // keep running until phase successful
+                try {
+                    phaseProfilerResult =
+                        profiler?.run {
+                            inMemoryTrace("start profiling") {
+                                startIfNotRiskingAnrDeadline(
+                                    traceUniqueName = traceUniqueName,
+                                    estimatedDurationNs = state.warmupEstimatedIterationTimeNs
+                                )
+                            }
+                        }
+                    state.metrics = metricsContainer // needed for pausing
+                    metricsContainer.captureInit()
+
+                    // warmup the container
+                    metricsContainer.captureStart()
+                    metricsContainer.captureStop()
+                    metricsContainer.captureInit()
+
+                    repeat(measurementCount) {
+                        // perform measurement
+                        metricsContainer.captureStart()
+                        loopedMeasurementBlock.invoke(scope, loopsPerMeasurement)
+                        metricsContainer.captureStop()
+                        state.yieldThreadIfDeadlinePassed()
+                    }
+                    if (loopMode.warmupManager != null) {
+                        // warmup, so retry until complete
+                        metricsContainer.captureInit()
+                        // Note that warmup is based on repeat time, *not* the timeNs metric, since
+                        // we
+                        // want to account for paused time during warmup (paused work should
+                        // stabilize
+                        // too)
+                        val lastMeasuredWarmupValue = metricsContainer.peekSingleRepeatTime()
+                        if (loopMode.warmupManager.onNextIteration(lastMeasuredWarmupValue)) {
+                            state.warmupEstimatedIterationTimeNs = lastMeasuredWarmupValue
+                            state.warmupIterations = loopMode.warmupManager.iteration
+                            break
+                        } else {
+                            continue
+                        }
+                    }
+                } finally {
+                    profiler?.run { inMemoryTrace("profiler.stop()") { stop() } }
+                    state.yieldThreadIfDeadlinePassed()
+                }
+                if (!ThrottleDetector.isDeviceThermalThrottled()) {
+                    // not thermal throttled, phase complete
+                    break
+                } else {
+                    // thermal throttled! delay and retry!
+                    Log.d(
+                        BenchmarkState.TAG,
+                        "THERMAL THROTTLE DETECTED, DELAYING FOR " +
+                            "${Arguments.thermalThrottleSleepDurationSeconds} SECONDS"
+                    )
+                    val startTimeNs = System.nanoTime()
+                    inMemoryTrace("Sleep due to Thermal Throttle") {
+                        delay(
+                            TimeUnit.SECONDS.toMillis(Arguments.thermalThrottleSleepDurationSeconds)
+                        )
+                    }
+                    val sleepTimeNs = System.nanoTime() - startTimeNs
+                    state.totalThermalThrottleSleepSeconds +=
+                        TimeUnit.NANOSECONDS.toSeconds(sleepTimeNs)
+                    thermalThrottleSleepsRemaining--
+                    if (thermalThrottleSleepsRemaining <= 0) break
+                }
+            }
+        } finally {
+            InMemoryTracing.endSection()
+        }
+        if (loopMode.warmupManager == null) {
+            // Save captured metrics except during warmup, where we intentionally discard
+            state.metricResults.addAll(
+                metricsContainer.captureFinished(maxIterations = loopsPerMeasurement)
+            )
+        }
+        if (phaseProfilerResult != null) {
+            state.profilerResults.add(phaseProfilerResult)
+        }
+    }
+
     internal sealed class LoopMode(val warmupManager: WarmupManager? = null) {
         /** Warmup looping mode - reports a single iteration, but there is specialized code in */
         class Warmup(warmupManager: WarmupManager) : LoopMode(warmupManager) {
@@ -207,6 +311,20 @@
         val measurementCount: Int?,
         val metrics: Array<MetricCapture>,
     ) {
+        constructor(
+            microbenchmarkConfig: MicrobenchmarkConfig,
+            simplifiedTimingOnlyMode: Boolean
+        ) : this(
+            dryRunMode = Arguments.dryRunMode,
+            startupMode = Arguments.startupMode,
+            profiler = microbenchmarkConfig.profiler?.profiler ?: Arguments.profiler,
+            profilerPerfCompareMode = Arguments.profilerPerfCompareEnable,
+            warmupCount = microbenchmarkConfig.warmupCount,
+            measurementCount = Arguments.iterations ?: microbenchmarkConfig.measurementCount,
+            simplifiedTimingOnlyMode = simplifiedTimingOnlyMode,
+            metrics = microbenchmarkConfig.metrics.toTypedArray()
+        )
+
         val warmupManager = WarmupManager(overrideCount = warmupCount)
 
         init {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
index 950f6d7..931af7b 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
@@ -19,10 +19,13 @@
 import android.annotation.SuppressLint
 import android.os.Build
 import android.os.Debug
+import android.os.Looper
 import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
+import androidx.benchmark.BenchmarkState.Companion.METHOD_TRACING_ESTIMATED_SLOWDOWN_FACTOR
+import androidx.benchmark.BenchmarkState.Companion.METHOD_TRACING_MAX_DURATION_NS
 import androidx.benchmark.BenchmarkState.Companion.TAG
 import androidx.benchmark.Outputs.dateToFileName
 import androidx.benchmark.json.BenchmarkData.TestResult.ProfilerOutput
@@ -99,6 +102,35 @@
 
     abstract fun start(traceUniqueName: String): ResultFile?
 
+    /** Start profiling only if expected trace duration is unlikely to trigger an ANR */
+    fun startIfNotRiskingAnrDeadline(
+        traceUniqueName: String,
+        estimatedDurationNs: Long
+    ): ResultFile? {
+        val estimatedMethodTraceDurNs =
+            estimatedDurationNs * METHOD_TRACING_ESTIMATED_SLOWDOWN_FACTOR
+        return if (
+            this == MethodTracing &&
+                Looper.myLooper() == Looper.getMainLooper() &&
+                estimatedMethodTraceDurNs > METHOD_TRACING_MAX_DURATION_NS &&
+                Arguments.profilerSkipWhenDurationRisksAnr
+        ) {
+            val expectedDurSec = estimatedMethodTraceDurNs / 1_000_000_000.0
+            InstrumentationResults.scheduleIdeWarningOnNextReport(
+                """
+                        Skipping method trace of estimated duration $expectedDurSec sec to avoid ANR
+
+                        To disable this behavior, set instrumentation arg:
+                            androidx.benchmark.profiling.skipWhenDurationRisksAnr = false
+                    """
+                    .trimIndent()
+            )
+            null
+        } else {
+            start(traceUniqueName)
+        }
+    }
+
     abstract fun stop()
 
     internal open fun config(packageNames: List<String>): StackSamplingConfig? = null
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/TestDefinition.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/TestDefinition.kt
new file mode 100644
index 0000000..69ff620
--- /dev/null
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/TestDefinition.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.benchmark
+
+import androidx.annotation.RestrictTo
+import androidx.benchmark.Errors.PREFIX
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class TestDefinition(
+    val fullClassName: String,
+    val simpleClassName: String,
+    val methodName: String,
+) {
+    val outputMethodName: String = PREFIX + methodName
+    val outputTestName: String = "$PREFIX$simpleClassName.$methodName"
+    val fullNameUnsanitized: String = "$fullClassName#$methodName"
+    val traceUniqueName: String = simpleClassName + "_" + methodName
+}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ThrottleDetector.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ThrottleDetector.kt
index f89e6cc..9b0b435 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ThrottleDetector.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ThrottleDetector.kt
@@ -33,12 +33,12 @@
         repeat(10) { System.arraycopy(a, 0, b, 0, a.size) }
     }
 
-    internal fun measureWorkNs(): Double {
+    internal fun measureWorkNs1(): Double {
         // Access a non-trivial amount of data to try and 'reset' any cache state.
         // Have observed this to give more consistent performance when clocks are unlocked.
         copySomeData()
 
-        val state = BenchmarkState(simplifiedTimingOnlyMode = true)
+        val state = BenchmarkStateLegacy(simplifiedTimingOnlyMode = true)
         val sourceMatrix = FloatArray(16) { System.nanoTime().toFloat() }
         val resultMatrix = FloatArray(16)
 
@@ -50,6 +50,20 @@
         return state.getMinTimeNanos()
     }
 
+    internal fun measureWorkNs(): Double =
+        inMemoryTrace("measureWorkNs") {
+            // Access a non-trivial amount of data to try and 'reset' any cache state.
+            // Have observed this to give more consistent performance when clocks are unlocked.
+            copySomeData()
+            val sourceMatrix = FloatArray(16) { System.nanoTime().toFloat() }
+            val resultMatrix = FloatArray(16)
+
+            return measureRepeatedCheckNanosReentrant {
+                // Benchmark a trivial consistent task
+                Matrix.translateM(resultMatrix, 0, sourceMatrix, 0, 1F, 2F, 3F)
+            }
+        }
+
     /**
      * Called to calculate throttling baseline, will be ignored after first call until reset.
      *
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
index 0bfa464..7dd4e03 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
@@ -34,6 +34,9 @@
 import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_SUCCESS
 import java.io.FileOutputStream
 import java.lang.RuntimeException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
 
 /** Wrapper for [PerfettoCapture] which does nothing below API 23. */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -109,6 +112,7 @@
         }
     }
 
+    @OptIn(ExperimentalContracts::class)
     fun record(
         fileLabel: String,
         config: PerfettoConfig,
@@ -118,6 +122,7 @@
         inMemoryTracingLabel: String? = null,
         block: () -> Unit
     ): String? {
+        contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
         // skip if Perfetto not supported, or if caller opts out
         if (Build.VERSION.SDK_INT < 23 || !isAbiSupported() || !enableTracing) {
             block()
diff --git a/benchmark/benchmark-junit4/api/current.txt b/benchmark/benchmark-junit4/api/current.txt
index 9cf3391..9f33a3c 100644
--- a/benchmark/benchmark-junit4/api/current.txt
+++ b/benchmark/benchmark-junit4/api/current.txt
@@ -9,10 +9,15 @@
     ctor public BenchmarkRule();
     ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public BenchmarkRule(androidx.benchmark.MicrobenchmarkConfig config);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public androidx.benchmark.MicrobenchmarkConfig getConfig();
     method public androidx.benchmark.BenchmarkState getState();
+    property public final androidx.benchmark.MicrobenchmarkConfig config;
   }
 
   public final class BenchmarkRule.Scope {
+    method public final void pauseMeasurement();
+    method public final void resumeMeasurement();
+    method public final <T> T! runWithMeasurementDisabled(kotlin.jvm.functions.Function0<? extends T!>);
     method public inline <T> T runWithTimingDisabled(kotlin.jvm.functions.Function0<? extends T> block);
   }
 
diff --git a/benchmark/benchmark-junit4/api/restricted_current.txt b/benchmark/benchmark-junit4/api/restricted_current.txt
index 13794ef..e34a578 100644
--- a/benchmark/benchmark-junit4/api/restricted_current.txt
+++ b/benchmark/benchmark-junit4/api/restricted_current.txt
@@ -9,11 +9,18 @@
     ctor public BenchmarkRule();
     ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public BenchmarkRule(androidx.benchmark.MicrobenchmarkConfig config);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public androidx.benchmark.MicrobenchmarkConfig getConfig();
     method public androidx.benchmark.BenchmarkState getState();
+    property public final androidx.benchmark.MicrobenchmarkConfig config;
+    property @kotlin.PublishedApi internal final androidx.benchmark.TestDefinition? testDefinition;
+    field @kotlin.PublishedApi internal androidx.benchmark.TestDefinition? testDefinition;
   }
 
   public final class BenchmarkRule.Scope {
     method @kotlin.PublishedApi internal androidx.benchmark.BenchmarkState getOuterState();
+    method public final void pauseMeasurement();
+    method public final void resumeMeasurement();
+    method public final <T> T! runWithMeasurementDisabled(kotlin.jvm.functions.Function0<? extends T!>);
     method public inline <T> T runWithTimingDisabled(kotlin.jvm.functions.Function0<? extends T> block);
   }
 
diff --git a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleAnnotationTest.kt b/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleAnnotationTest.kt
index 59039df..ed17189 100644
--- a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleAnnotationTest.kt
+++ b/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleAnnotationTest.kt
@@ -37,4 +37,9 @@
     fun throwsIfNotAnnotatedMeasure() {
         unannotatedRule.measureRepeated {}
     }
+
+    @Test(expected = IllegalStateException::class)
+    fun throwsIfNotAnnotatedMeasureMain() {
+        unannotatedRule.measureRepeatedOnMainThread {}
+    }
 }
diff --git a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleLegacyTest.kt b/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleLegacyTest.kt
new file mode 100644
index 0000000..84ccc31
--- /dev/null
+++ b/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleLegacyTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.junit4
+
+import android.annotation.SuppressLint
+import android.os.Looper
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SmallTest
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class BenchmarkRuleLegacyTest {
+    @get:Rule val benchmarkRule = BenchmarkRuleLegacy()
+
+    @SuppressLint("BanThreadSleep") // doesn't affect runtime, since we have target time
+    @Test
+    fun runWithTimingDisabled() {
+        benchmarkRule.measureRepeated { runWithTimingDisabled { Thread.sleep(5) } }
+        val min = benchmarkRule.getState().getMinTimeNanos()
+        Assert.assertTrue(
+            "minimum $min should be less than 1ms",
+            min < TimeUnit.MILLISECONDS.toNanos(1)
+        )
+    }
+
+    @Test
+    fun measureRepeatedMainThread() {
+        var scheduledOnMain = false
+
+        // validate rethrow behavior
+        assertFailsWith<IllegalStateException> {
+            benchmarkRule.measureRepeatedOnMainThread {
+                scheduledOnMain = Looper.myLooper() == Looper.getMainLooper()
+
+                throw IllegalStateException("just a test")
+            }
+        }
+
+        // validate work done on main thread
+        assertTrue(scheduledOnMain)
+
+        // let a benchmark actually run, so "benchmark hasn't finished" isn't thrown
+        benchmarkRule.measureRepeatedOnMainThread {}
+    }
+
+    @SmallTest
+    @Test
+    @UiThreadTest
+    fun measureRepeatedOnMainThread_throwOnMain() {
+        assertEquals(Looper.myLooper(), Looper.getMainLooper())
+        // validate rethrow behavior
+        val exception =
+            assertFailsWith<IllegalStateException> {
+                benchmarkRule.measureRepeatedOnMainThread {
+                    // Doesn't matter
+                }
+            }
+        assertTrue(
+            exception.message!!.contains(
+                "Cannot invoke measureRepeatedOnMainThread from the main thread"
+            )
+        )
+    }
+}
diff --git a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleTest.kt b/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleTest.kt
index 8adb35b..f0fa7fe 100644
--- a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleTest.kt
+++ b/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/BenchmarkRuleTest.kt
@@ -16,17 +16,14 @@
 
 package androidx.benchmark.junit4
 
-import android.annotation.SuppressLint
 import android.os.Looper
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SmallTest
-import java.util.concurrent.TimeUnit
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertTrue
-import org.junit.Assert
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,17 +33,6 @@
 class BenchmarkRuleTest {
     @get:Rule val benchmarkRule: BenchmarkRule = BenchmarkRule()
 
-    @SuppressLint("BanThreadSleep") // doesn't affect runtime, since we have target time
-    @Test
-    fun runWithTimingDisabled() {
-        benchmarkRule.measureRepeated { runWithTimingDisabled { Thread.sleep(5) } }
-        val min = benchmarkRule.getState().getMinTimeNanos()
-        Assert.assertTrue(
-            "minimum $min should be less than 1ms",
-            min < TimeUnit.MILLISECONDS.toNanos(1)
-        )
-    }
-
     @Test
     fun measureRepeatedMainThread() {
         var scheduledOnMain = false
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index b1afdf0..ac56c23 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -17,30 +17,18 @@
 package androidx.benchmark.junit4
 
 import android.Manifest
-import android.os.Build
 import android.os.Looper
-import android.util.Log
 import androidx.annotation.RestrictTo
 import androidx.benchmark.Arguments
 import androidx.benchmark.BenchmarkState
 import androidx.benchmark.DeviceInfo
 import androidx.benchmark.ExperimentalBenchmarkConfigApi
 import androidx.benchmark.MicrobenchmarkConfig
-import androidx.benchmark.perfetto.PerfettoCapture
-import androidx.benchmark.perfetto.PerfettoCaptureWrapper
-import androidx.benchmark.perfetto.PerfettoConfig
-import androidx.benchmark.perfetto.UiState
-import androidx.benchmark.perfetto.appendUiState
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.benchmark.MicrobenchmarkRunningState
+import androidx.benchmark.MicrobenchmarkScope
+import androidx.benchmark.TestDefinition
+import androidx.benchmark.measureRepeatedImplWithTracing
 import androidx.test.rule.GrantPermissionRule
-import androidx.tracing.Trace
-import androidx.tracing.trace
-import java.io.File
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.FutureTask
-import java.util.concurrent.TimeUnit
-import org.junit.Assert.assertTrue
 import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeTrue
 import org.junit.rules.RuleChain
@@ -66,25 +54,31 @@
  * @sample androidx.benchmark.samples.benchmarkRuleSample
  */
 class BenchmarkRule
-private constructor(
-    private val config: MicrobenchmarkConfig?,
-    /**
-     * This param is ignored, and just present to disambiguate the internal (nullable) vs external
-     * (non-null) variants of the constructor, since a lint failure occurs if they have the same
-     * signature, even if the external variant uses `this(config as MicrobenchmarkConfig?)`.
-     *
-     * In the future, we should just always pass a "default" config object, which can reference
-     * default values from Arguments, but that's a deeper change.
-     */
-    @Suppress("UNUSED_PARAMETER") ignored: Boolean = true
+@ExperimentalBenchmarkConfigApi
+constructor(
+    val config: MicrobenchmarkConfig,
 ) : TestRule {
-    constructor() : this(config = null, ignored = true)
+    constructor() : this(config = MicrobenchmarkConfig())
 
-    @ExperimentalBenchmarkConfigApi
-    constructor(config: MicrobenchmarkConfig) : this(config, ignored = true)
+    @PublishedApi
+    internal // synthetic access
+    var testDefinition: TestDefinition? = null
+        get() {
+            throwIfNotApplied()
+            return field
+        }
 
     internal // synthetic access
-    var internalState = BenchmarkState(config)
+    var internalState: BenchmarkState? = null
+
+    internal fun throwIfNotApplied() {
+        if (!applied) {
+            throw IllegalStateException(
+                "Cannot get state before BenchmarkRule is applied to a test. Check that your " +
+                    "BenchmarkRule is annotated correctly (@Rule in Java, @get:Rule in Kotlin)."
+            )
+        }
+    }
 
     /**
      * Object used for benchmarking in Java.
@@ -106,56 +100,65 @@
      *
      * @throws [IllegalStateException] if the BenchmarkRule isn't correctly applied to a test.
      */
-    public fun getState(): BenchmarkState {
+    fun getState(): BenchmarkState {
         // Note: this is an explicit method instead of an accessor to help convey it's only for Java
         // Kotlin users should call the [measureRepeated] method.
-        if (!applied) {
-            throw IllegalStateException(
-                "Cannot get state before BenchmarkRule is applied to a test. Check that your " +
-                    "BenchmarkRule is annotated correctly (@Rule in Java, @get:Rule in Kotlin)."
-            )
-        }
-        return internalState
+        throwIfNotApplied()
+        return internalState!!
     }
 
     internal // synthetic access
     var applied = false
 
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY) public val scope: Scope = Scope()
+    // can we avoid published API here?
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    val scopeFactory: (MicrobenchmarkRunningState) -> MicrobenchmarkScope = { runningState ->
+        Scope(runningState)
+    }
 
-    /** Handle used for controlling timing during [measureRepeated]. */
-    public inner class Scope internal constructor() {
-        /**
-         * Disable timing for a block of code.
+    /** Handle used for controlling measurement during [measureRepeated]. */
+    inner class Scope internal constructor(internal val state: MicrobenchmarkRunningState) :
+
+        /*
+         * Ideally, the microbenchmark scope concept would live entirely in benchmark-common so that
+         * we can define it in common code, without a dependence on benchmark-junit / JUnit.
          *
-         * Used for disabling timing for work that isn't part of the benchmark:
+         * To preserve compatibility though, we have to preserve this copy, which causes the
+         * following layering compromises:
+         *
+         * 1. The top level `measureRepeated` function accepts a `scopeFactory` function to let it
+         * construct a BenchmarkRule.Scope object, even though it's a trivial wrapper around
+         * MicrobenchmarkScope.
+         *
+         * 2. To let scope-ish calls go from BenchmarkState -> MicrobenchmarkScope ->
+         * MicrobenchmarkRunningState, BenchmarkState has a LIBRARY_GROUP mutable var, so both
+         * legacy BenchmarkState.keepRunning() and modern BenchmarkRule.measureRepeated have to
+         * separately set BenchmarkState.scope after scope is constructed. This stinks, but it's the
+         * price of compat. Both are needed only because it was valid to pause timing in a pure
+         * Kotlin benchmark with rule.getState().pauseTiming()
+         */
+        MicrobenchmarkScope(state) {
+
+        /**
+         * Disable measurement for a block of code.
+         *
+         * Used for disabling timing/measurement for work that isn't part of the benchmark:
          * - When constructing per-loop randomized inputs for operations with caching,
          * - Controlling which parts of multi-stage work are measured (e.g. View measure/layout)
-         * - Disabling timing during per-loop verification
+         * - Per-loop verification
          *
          * @sample androidx.benchmark.samples.runWithTimingDisabledSample
          */
-        public inline fun <T> runWithTimingDisabled(block: () -> T): T {
-            getOuterState().pauseTiming()
-            // Note: we only bother with tracing for the runWithTimingDisabled function for
-            // Kotlin callers, as it's more difficult to corrupt the trace with incorrectly
-            // paired BenchmarkState pause/resume calls
-            val ret: T =
-                try {
-                    // TODO: use `trace() {}` instead of this manual try/finally,
-                    //  once the block parameter is marked crossinline.
-                    Trace.beginSection("runWithTimingDisabled")
-                    block()
-                } finally {
-                    Trace.endSection()
-                }
-            getOuterState().resumeTiming()
-            return ret
+        inline fun <T> runWithTimingDisabled(block: () -> T): T {
+            return runWithMeasurementDisabled(block)
         }
 
         /**
-         * Allows the inline function [runWithTimingDisabled] to be called outside of this scope.
+         * Allows the inline function [runWithTimingDisabled] to be called outside of this scope for
+         * compat with compiled code using old versions of the library.
          */
+        @Suppress("unused")
         @PublishedApi
         internal fun getOuterState(): BenchmarkState {
             return getState()
@@ -184,84 +187,18 @@
             )
         }
 
-        var invokeMethodName = description.methodName
-        Log.d(TAG, "-- Running ${description.className}#$invokeMethodName --")
+        testDefinition =
+            TestDefinition(
+                fullClassName = description.className,
+                simpleClassName = description.testClass.simpleName,
+                methodName = description.methodName
+            )
 
-        // validate and simplify the function name.
-        // First, remove the "test" prefix which normally comes from CTS test.
-        // Then make sure the [subTestName] is valid, not just numbers like [0].
-        if (invokeMethodName.startsWith("test")) {
-            assertTrue("The test name $invokeMethodName is too short", invokeMethodName.length > 5)
-            invokeMethodName =
-                invokeMethodName.substring(4, 5).lowercase() + invokeMethodName.substring(5)
-        }
-        val uniqueName = description.testClass.simpleName + "_" + invokeMethodName
-        internalState.traceUniqueName = uniqueName
+        // only used with legacy getState() API, which is intended to be deprecated in the future,
+        // to be replaced by Java variant of measureRepeated
+        internalState = BenchmarkState(testDefinition!!, config)
 
-        val tracePath =
-            PerfettoCaptureWrapper()
-                .record(
-                    fileLabel = uniqueName,
-                    config =
-                        PerfettoConfig.Benchmark(
-                            appTagPackages =
-                                if (config?.traceAppTagEnabled == true) {
-                                    listOf(
-                                        InstrumentationRegistry.getInstrumentation()
-                                            .context
-                                            .packageName
-                                    )
-                                } else {
-                                    emptyList()
-                                },
-                            useStackSamplingConfig = false
-                        ),
-                    // TODO(290918736): add support for Perfetto SDK Tracing in
-                    //  Microbenchmark in other cases, outside of MicrobenchmarkConfig
-                    perfettoSdkConfig =
-                        if (
-                            config?.perfettoSdkTracingEnabled == true &&
-                                Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-                        ) {
-                            PerfettoCapture.PerfettoSdkConfig(
-                                getInstrumentation().context.packageName,
-                                PerfettoCapture.PerfettoSdkConfig.InitialProcessState.Alive
-                            )
-                        } else {
-                            null
-                        },
-
-                    // Optimize throughput in dryRunMode, since trace isn't useful, and extremely
-                    //   expensive on some emulators. Could alternately use UserspaceTracing if
-                    // desired
-                    // Additionally, skip on misconfigured devices to still enable benchmarking.
-                    enableTracing = !Arguments.dryRunMode && !DeviceInfo.misconfiguredForTracing,
-                    inMemoryTracingLabel = "Microbenchmark"
-                ) {
-                    trace(description.displayName) { base.evaluate() }
-                }
-                ?.apply {
-                    // trace completed, and copied into shell writeable dir
-                    val file = File(this)
-                    file.appendUiState(
-                        UiState(
-                            timelineStart = null,
-                            timelineEnd = null,
-                            highlightPackage = getInstrumentation().context.packageName
-                        )
-                    )
-                }
-
-        internalState.report(
-            fullClassName = description.className,
-            simpleClassName = description.testClass.simpleName,
-            methodName = invokeMethodName,
-            perfettoTracePath = tracePath
-        )
-    }
-
-    internal companion object {
-        private const val TAG = "Benchmark"
+        base.evaluate()
     }
 }
 
@@ -273,26 +210,27 @@
  */
 public inline fun BenchmarkRule.measureRepeated(crossinline block: BenchmarkRule.Scope.() -> Unit) {
     // Note: this is an extension function to discourage calling from Java.
-
     if (Arguments.throwOnMainThreadMeasureRepeated) {
         check(Looper.myLooper() != Looper.getMainLooper()) {
             "Cannot invoke measureRepeated from the main thread. Instead use" +
                 " measureRepeatedOnMainThread()"
         }
     }
-
-    // Extract members to locals, to ensure we check #applied, and we don't hit accessors
-    val localState = getState()
-    val localScope = scope
-
-    try {
-        while (localState.keepRunningInline()) {
-            block(localScope)
+    measureRepeatedImplWithTracing(
+        postToMainThread = false,
+        definition = testDefinition!!,
+        config = config,
+        scopeFactory = scopeFactory, // inflate custom Scope object to respect/maintain public API
+        loopedMeasurementBlock = { scope, iterations ->
+            val ruleScope = scope as BenchmarkRule.Scope // cast back to outer scope type
+            getState().scope = scope
+            var remainingIterations = iterations
+            do {
+                block.invoke(ruleScope)
+                remainingIterations--
+            } while (remainingIterations > 0)
         }
-    } catch (t: Throwable) {
-        localState.cleanupBeforeThrow()
-        throw t
-    }
+    )
 }
 
 /**
@@ -315,76 +253,21 @@
         "Cannot invoke measureRepeatedOnMainThread from the main thread"
     }
 
-    var resumeScheduled = false
-    while (true) {
-        val task = FutureTask {
-            // Extract members to locals, to ensure we check #applied, and we don't hit accessors
-            val localState = getState()
-            val localScope = scope
-
-            val initialTimeNs = System.nanoTime()
-            // we try to stop next measurement after soft deadline...
-            val softDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(2)
-            // ... and throw if took longer than hard deadline
-            val hardDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(10)
-            var timeNs: Long = 0
-
-            try {
-                Trace.beginSection("measureRepeatedOnMainThread task")
-
-                if (resumeScheduled) {
-                    localState.resumeTiming()
-                }
-
-                do {
-                    // note that this function can still block for considerable time, e.g. when
-                    // setting up / tearing down profiling, or sleeping to let the device cool off.
-                    if (!localState.keepRunningInline()) {
-                        return@FutureTask false
-                    }
-
-                    block(localScope)
-
-                    // Avoid checking for deadline on all but last iteration per measurement,
-                    // to amortize cost of System.nanoTime(). Without this optimization, minimum
-                    // measured time can be 10x higher.
-                    if (localState.getIterationsRemaining() != 1) {
-                        continue
-                    }
-                    timeNs = System.nanoTime()
-                } while (timeNs <= softDeadlineNs)
-
-                resumeScheduled = true
-                localState.pauseTiming()
-
-                if (timeNs > hardDeadlineNs && Arguments.measureRepeatedOnMainThrowOnDeadline) {
-                    localState.cleanupBeforeThrow()
-                    val overrunInSec = (timeNs - hardDeadlineNs) / 1_000_000_000.0
-                    throw IllegalStateException(
-                        "Benchmark loop overran hard time limit by $overrunInSec seconds"
-                    )
-                }
-
-                return@FutureTask true // continue
-            } finally {
-                Trace.endSection()
-            }
+    measureRepeatedImplWithTracing(
+        postToMainThread = true,
+        definition = testDefinition!!,
+        config = config,
+        scopeFactory = scopeFactory, // inflate custom Scope object to respect/maintain public API
+        loopedMeasurementBlock = { scope, iterations ->
+            val ruleScope = scope as BenchmarkRule.Scope // cast back to outer scope type
+            getState().scope = scope
+            var remainingIterations = iterations
+            do {
+                block.invoke(ruleScope)
+                remainingIterations--
+            } while (remainingIterations > 0)
         }
-        getInstrumentation().runOnMainSync(task)
-        val shouldContinue: Boolean =
-            try {
-                // Ideally we'd implement the delay here, as a timeout, but we can't do this until
-                // have a way to move thermal throttle sleeping off the UI thread.
-                task.get()
-            } catch (e: ExecutionException) {
-                // Expose the original exception
-                throw e.cause!!
-            }
-        if (!shouldContinue) {
-            // all done
-            break
-        }
-    }
+    )
 }
 
 internal inline fun Statement(crossinline evaluate: () -> Unit) =
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRuleLegacy.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRuleLegacy.kt
new file mode 100644
index 0000000..cdbe16e
--- /dev/null
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRuleLegacy.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.junit4
+
+import android.Manifest
+import android.os.Build
+import android.os.Looper
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.benchmark.Arguments
+import androidx.benchmark.BenchmarkStateLegacy
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.ExperimentalBenchmarkConfigApi
+import androidx.benchmark.MicrobenchmarkConfig
+import androidx.benchmark.perfetto.PerfettoCapture
+import androidx.benchmark.perfetto.PerfettoCaptureWrapper
+import androidx.benchmark.perfetto.PerfettoConfig
+import androidx.benchmark.perfetto.UiState
+import androidx.benchmark.perfetto.appendUiState
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.test.rule.GrantPermissionRule
+import androidx.tracing.Trace
+import androidx.tracing.trace
+import java.io.File
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.FutureTask
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * JUnit rule for benchmarking code on an Android device.
+ *
+ * In Kotlin, benchmark with [measureRepeated]. In Java, use [getState].
+ *
+ * Benchmark results will be output:
+ * - Summary in Android Studio in the test log
+ * - In JSON format, on the host
+ * - In simple form in Logcat with the tag "Benchmark"
+ *
+ * Every test in the Class using this @Rule must contain a single benchmark.
+ *
+ * See the [Benchmark Guide](https://developer.android.com/studio/profile/benchmark) for more
+ * information on writing Benchmarks.
+ *
+ * @sample androidx.benchmark.samples.benchmarkRuleSample
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class BenchmarkRuleLegacy
+private constructor(
+    private val config: MicrobenchmarkConfig?,
+    /**
+     * This param is ignored, and just present to disambiguate the internal (nullable) vs external
+     * (non-null) variants of the constructor, since a lint failure occurs if they have the same
+     * signature, even if the external variant uses `this(config as MicrobenchmarkConfig?)`.
+     *
+     * In the future, we should just always pass a "default" config object, which can reference
+     * default values from Arguments, but that's a deeper change.
+     */
+    @Suppress("UNUSED_PARAMETER") ignored: Boolean = true
+) : TestRule {
+    constructor() : this(config = null, ignored = true)
+
+    @ExperimentalBenchmarkConfigApi
+    constructor(config: MicrobenchmarkConfig) : this(config, ignored = true)
+
+    internal // synthetic access
+    var internalState = BenchmarkStateLegacy(config)
+
+    /**
+     * Object used for benchmarking in Java.
+     *
+     * ```java
+     * @Rule
+     * public BenchmarkRule benchmarkRule = new BenchmarkRule();
+     *
+     * @Test
+     * public void myBenchmark() {
+     *     ...
+     *     BenchmarkState state = benchmarkRule.getBenchmarkState();
+     *     while (state.keepRunning()) {
+     *         doSomeWork();
+     *     }
+     *     ...
+     * }
+     * ```
+     *
+     * @throws [IllegalStateException] if the BenchmarkRule isn't correctly applied to a test.
+     */
+    public fun getState(): BenchmarkStateLegacy {
+        // Note: this is an explicit method instead of an accessor to help convey it's only for Java
+        // Kotlin users should call the [measureRepeated] method.
+        if (!applied) {
+            throw IllegalStateException(
+                "Cannot get state before BenchmarkRule is applied to a test. Check that your " +
+                    "BenchmarkRule is annotated correctly (@Rule in Java, @get:Rule in Kotlin)."
+            )
+        }
+        return internalState
+    }
+
+    internal // synthetic access
+    var applied = false
+
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY) public val scope: Scope = Scope()
+
+    /** Handle used for controlling timing during [measureRepeated]. */
+    inner class Scope internal constructor() {
+        /**
+         * Disable timing for a block of code.
+         *
+         * Used for disabling timing for work that isn't part of the benchmark:
+         * - When constructing per-loop randomized inputs for operations with caching,
+         * - Controlling which parts of multi-stage work are measured (e.g. View measure/layout)
+         * - Disabling timing during per-loop verification
+         *
+         * @sample androidx.benchmark.samples.runWithTimingDisabledSample
+         */
+        public inline fun <T> runWithTimingDisabled(block: () -> T): T {
+            getOuterState().pauseTiming()
+            // Note: we only bother with tracing for the runWithTimingDisabled function for
+            // Kotlin callers, as it's more difficult to corrupt the trace with incorrectly
+            // paired BenchmarkState pause/resume calls
+            val ret: T =
+                try {
+                    // TODO: use `trace() {}` instead of this manual try/finally,
+                    //  once the block parameter is marked crossinline.
+                    Trace.beginSection("runWithTimingDisabled")
+                    block()
+                } finally {
+                    Trace.endSection()
+                }
+            getOuterState().resumeTiming()
+            return ret
+        }
+
+        /**
+         * Allows the inline function [runWithTimingDisabled] to be called outside of this scope.
+         */
+        @Suppress("ShowingMemberInHiddenClass")
+        @PublishedApi
+        internal fun getOuterState(): BenchmarkStateLegacy {
+            return getState()
+        }
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return RuleChain.outerRule(
+                GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            )
+            .around(::applyInternal)
+            .apply(base, description)
+    }
+
+    private fun applyInternal(base: Statement, description: Description) = Statement {
+        applied = true
+
+        assumeTrue(Arguments.RuleType.Microbenchmark in Arguments.enabledRules)
+
+        // When running on emulator and argument `skipOnEmulator` is passed,
+        // the test is skipped.
+        if (Arguments.skipBenchmarksOnEmulator) {
+            assumeFalse(
+                "Skipping test because it's running on emulator and `skipOnEmulator` is enabled",
+                DeviceInfo.isEmulator
+            )
+        }
+
+        var invokeMethodName = description.methodName
+        Log.d(TAG, "-- Running ${description.className}#$invokeMethodName --")
+
+        // validate and simplify the function name.
+        // First, remove the "test" prefix which normally comes from CTS test.
+        // Then make sure the [subTestName] is valid, not just numbers like [0].
+        if (invokeMethodName.startsWith("test")) {
+            assertTrue("The test name $invokeMethodName is too short", invokeMethodName.length > 5)
+            invokeMethodName =
+                invokeMethodName.substring(4, 5).lowercase() + invokeMethodName.substring(5)
+        }
+        val uniqueName = description.testClass.simpleName + "_" + invokeMethodName
+        internalState.traceUniqueName = uniqueName
+
+        val tracePath =
+            PerfettoCaptureWrapper()
+                .record(
+                    fileLabel = uniqueName,
+                    config =
+                        PerfettoConfig.Benchmark(
+                            appTagPackages =
+                                if (config?.traceAppTagEnabled == true) {
+                                    listOf(getInstrumentation().context.packageName)
+                                } else {
+                                    emptyList()
+                                },
+                            useStackSamplingConfig = false
+                        ),
+                    // TODO(290918736): add support for Perfetto SDK Tracing in
+                    //  Microbenchmark in other cases, outside of MicrobenchmarkConfig
+                    perfettoSdkConfig =
+                        if (
+                            config?.perfettoSdkTracingEnabled == true &&
+                                Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                        ) {
+                            PerfettoCapture.PerfettoSdkConfig(
+                                getInstrumentation().context.packageName,
+                                PerfettoCapture.PerfettoSdkConfig.InitialProcessState.Alive
+                            )
+                        } else {
+                            null
+                        },
+
+                    // Optimize throughput in dryRunMode, since trace isn't useful, and extremely
+                    //   expensive on some emulators. Could alternately use UserspaceTracing if
+                    // desired
+                    // Additionally, skip on misconfigured devices to still enable benchmarking.
+                    enableTracing = !Arguments.dryRunMode && !DeviceInfo.misconfiguredForTracing,
+                    inMemoryTracingLabel = "Microbenchmark"
+                ) {
+                    trace(description.displayName) { base.evaluate() }
+                }
+                ?.apply {
+                    // trace completed, and copied into shell writeable dir
+                    val file = File(this)
+                    file.appendUiState(
+                        UiState(
+                            timelineStart = null,
+                            timelineEnd = null,
+                            highlightPackage = getInstrumentation().context.packageName
+                        )
+                    )
+                }
+
+        internalState.report(
+            fullClassName = description.className,
+            simpleClassName = description.testClass.simpleName,
+            methodName = invokeMethodName,
+            perfettoTracePath = tracePath
+        )
+    }
+
+    internal companion object {
+        private const val TAG = "Benchmark"
+    }
+}
+
+/**
+ * Benchmark a block of code.
+ *
+ * @param block The block of code to benchmark.
+ * @sample androidx.benchmark.samples.benchmarkRuleSample
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+inline fun BenchmarkRuleLegacy.measureRepeated(
+    crossinline block: BenchmarkRuleLegacy.Scope.() -> Unit
+) {
+    // Note: this is an extension function to discourage calling from Java.
+
+    if (Arguments.throwOnMainThreadMeasureRepeated) {
+        check(Looper.myLooper() != Looper.getMainLooper()) {
+            "Cannot invoke measureRepeated from the main thread. Instead use" +
+                " measureRepeatedOnMainThread()"
+        }
+    }
+
+    // Extract members to locals, to ensure we check #applied, and we don't hit accessors
+    val localState = getState()
+    val localScope = scope
+
+    try {
+        while (localState.keepRunningInline()) {
+            block(localScope)
+        }
+    } catch (t: Throwable) {
+        localState.cleanupBeforeThrow()
+        throw t
+    }
+}
+
+/**
+ * Benchmark a block of code, which runs on the main thread, and can safely interact with UI.
+ *
+ * While `@UiThreadRule` works for a standard test, it doesn't work for benchmarks of arbitrary
+ * duration, as they may run for much more than 5 seconds and suffer ANRs, especially in continuous
+ * runs.
+ *
+ * @param block The block of code to benchmark.
+ * @throws java.lang.Throwable when an exception is thrown on the main thread.
+ * @throws IllegalStateException if a hard deadline is exceeded while the block is running on the
+ *   main thread.
+ * @sample androidx.benchmark.samples.measureRepeatedOnMainThreadSample
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+inline fun BenchmarkRuleLegacy.measureRepeatedOnMainThread(
+    crossinline block: BenchmarkRuleLegacy.Scope.() -> Unit
+) {
+    check(Looper.myLooper() != Looper.getMainLooper()) {
+        "Cannot invoke measureRepeatedOnMainThread from the main thread"
+    }
+
+    var resumeScheduled = false
+    while (true) {
+        val task = FutureTask {
+            // Extract members to locals, to ensure we check #applied, and we don't hit accessors
+            val localState = getState()
+            val localScope = scope
+
+            val initialTimeNs = System.nanoTime()
+            // we try to stop next measurement after soft deadline...
+            val softDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(2)
+            // ... and throw if took longer than hard deadline
+            val hardDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(10)
+            var timeNs: Long = 0
+
+            try {
+                Trace.beginSection("measureRepeatedOnMainThread task")
+
+                if (resumeScheduled) {
+                    localState.resumeTiming()
+                }
+
+                do {
+                    // note that this function can still block for considerable time, e.g. when
+                    // setting up / tearing down profiling, or sleeping to let the device cool off.
+                    if (!localState.keepRunningInline()) {
+                        return@FutureTask false
+                    }
+
+                    block(localScope)
+
+                    // Avoid checking for deadline on all but last iteration per measurement,
+                    // to amortize cost of System.nanoTime(). Without this optimization, minimum
+                    // measured time can be 10x higher.
+                    if (localState.getIterationsRemaining() != 1) {
+                        continue
+                    }
+                    timeNs = System.nanoTime()
+                } while (timeNs <= softDeadlineNs)
+
+                resumeScheduled = true
+                localState.pauseTiming()
+
+                if (timeNs > hardDeadlineNs && Arguments.measureRepeatedOnMainThrowOnDeadline) {
+                    localState.cleanupBeforeThrow()
+                    val overrunInSec = (timeNs - hardDeadlineNs) / 1_000_000_000.0
+                    throw IllegalStateException(
+                        "Benchmark loop overran hard time limit by $overrunInSec seconds"
+                    )
+                }
+
+                return@FutureTask true // continue
+            } finally {
+                Trace.endSection()
+            }
+        }
+        getInstrumentation().runOnMainSync(task)
+        val shouldContinue: Boolean =
+            try {
+                // Ideally we'd implement the delay here, as a timeout, but we can't do this until
+                // have a way to move thermal throttle sleeping off the UI thread.
+                task.get()
+            } catch (e: ExecutionException) {
+                // Expose the original exception
+                throw e.cause!!
+            }
+        if (!shouldContinue) {
+            // all done
+            break
+        }
+    }
+}
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkStateBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkStateBenchmark.kt
index a1d8c31..f71a6a7 100644
--- a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkStateBenchmark.kt
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkStateBenchmark.kt
@@ -17,7 +17,7 @@
 package androidx.benchmark.benchmark
 
 import androidx.benchmark.BenchmarkState
-import androidx.benchmark.ExperimentalBenchmarkStateApi
+import androidx.benchmark.TestDefinition
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import org.junit.Test
@@ -26,13 +26,19 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class BenchmarkStateBenchmark {
-    @OptIn(ExperimentalBenchmarkStateApi::class)
+    /** Proof of concept benchmark using BenchmarkState without a JUnit Rule */
     @Test
     fun nothing() {
-        val state = BenchmarkState(warmupCount = 10, repeatCount = 10)
+        val state =
+            BenchmarkState(
+                TestDefinition(
+                    "androidx.benchmark.benchmark.BenchmarkState2Benchmark",
+                    "BenchmarkState2Benchmark",
+                    "increment"
+                ),
+            )
         while (state.keepRunning()) {
-            // do nothing
+            //
         }
-        state.getMeasurementTimeNs()
     }
 }
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkStateLegacyBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkStateLegacyBenchmark.kt
new file mode 100644
index 0000000..2824881
--- /dev/null
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkStateLegacyBenchmark.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.benchmark
+
+import androidx.benchmark.BenchmarkStateLegacy
+import androidx.benchmark.ExperimentalBenchmarkStateApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class BenchmarkStateLegacyBenchmark {
+    @OptIn(ExperimentalBenchmarkStateApi::class)
+    @Test
+    fun nothing() {
+        val state = BenchmarkStateLegacy(warmupCount = 10, repeatCount = 10)
+        while (state.keepRunning()) {
+            // do nothing
+        }
+        state.getMeasurementTimeNs()
+    }
+}
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/MeasureRepeatedSampleBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/MeasureRepeatedSampleBenchmark.kt
new file mode 100644
index 0000000..7cbe0c6
--- /dev/null
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/MeasureRepeatedSampleBenchmark.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.benchmark
+
+import androidx.benchmark.ExperimentalBenchmarkConfigApi
+import androidx.benchmark.ExperimentalBlackHoleApi
+import androidx.benchmark.TestDefinition
+import androidx.benchmark.measureRepeated
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalBenchmarkConfigApi::class, ExperimentalBlackHoleApi::class)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class MeasureRepeatedSampleBenchmark {
+    /** Proof of concept of top-level benchmark function, without a JUnit Rule */
+    @Test
+    fun increment() {
+        println("increment")
+        var i: Int = 0
+        measureRepeated(
+            TestDefinition(
+                "androidx.benchmark.benchmark.MeasureRepeatedSampleBenchmark",
+                "MeasureRepeatedSampleBenchmark",
+                "increment"
+            )
+        ) {
+            i++
+        }
+    }
+}
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/TrivialKotlinBenchmarkLegacy.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/TrivialKotlinBenchmarkLegacy.kt
new file mode 100644
index 0000000..97c91c3
--- /dev/null
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/TrivialKotlinBenchmarkLegacy.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.benchmark
+
+import android.annotation.SuppressLint
+import androidx.benchmark.junit4.BenchmarkRuleLegacy
+import androidx.benchmark.junit4.measureRepeated
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class TrivialKotlinBenchmarkLegacy {
+    @get:Rule val benchmarkRule = BenchmarkRuleLegacy()
+
+    @SuppressLint("BanThreadSleep") // intentional bad behavior / regression
+    @Test
+    fun nothing() = benchmarkRule.measureRepeated { Thread.sleep(1) }
+
+    @Test
+    fun increment() {
+        var i = 0
+        benchmarkRule.measureRepeated { i++ }
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
index 885e0de..3ed68f0 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
@@ -18,7 +18,6 @@
 
 import androidx.build.Version
 import androidx.build.checkapi.ApiLocation
-import androidx.build.checkapi.StandardCompilationInputs
 import java.io.File
 import javax.inject.Inject
 import org.gradle.api.file.Directory
@@ -79,13 +78,6 @@
             check(compiled.exists()) { "File " + compiled + " does not exist" }
         }
 
-        val inputs =
-            StandardCompilationInputs(
-                sourcePaths = sourcePaths,
-                dependencyClasspath = dependencyClasspath,
-                bootClasspath = bootClasspath
-            )
-
         val levelsArgs =
             getGenerateApiLevelsArgs(
                 getPastApiFiles(),
@@ -96,7 +88,7 @@
         generateApi(
             metalavaClasspath,
             createProjectXmlFile(),
-            inputs,
+            sourcePaths.files,
             apiLocation.get(),
             ApiLintMode.CheckBaseline(baselines.get().apiLintFile, targetsJavaConsumers.get()),
             generateRestrictToLibraryGroupAPIs,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
index 580c652..3325d22 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
@@ -18,7 +18,6 @@
 
 import androidx.build.Version
 import androidx.build.checkapi.ApiLocation
-import androidx.build.checkapi.CompilationInputs
 import androidx.build.getLibraryByName
 import androidx.build.logging.TERMINAL_RED
 import androidx.build.logging.TERMINAL_RESET
@@ -243,8 +242,8 @@
  */
 internal fun generateApi(
     metalavaClasspath: FileCollection,
-    projectXml: File?,
-    files: CompilationInputs,
+    projectXml: File,
+    sourcePaths: Collection<File>,
     apiLocation: ApiLocation,
     apiLintMode: ApiLintMode,
     includeRestrictToLibraryGroupApis: Boolean,
@@ -268,7 +267,7 @@
         generateApi(
             metalavaClasspath,
             projectXml,
-            files,
+            sourcePaths,
             apiLocation,
             generateApiMode,
             apiLintMode,
@@ -287,8 +286,8 @@
  */
 private fun generateApi(
     metalavaClasspath: FileCollection,
-    projectXml: File?,
-    files: CompilationInputs,
+    projectXml: File,
+    sourcePaths: Collection<File>,
     outputLocation: ApiLocation,
     generateApiMode: GenerateApiMode,
     apiLintMode: ApiLintMode,
@@ -300,10 +299,8 @@
 ) {
     val args =
         getGenerateApiArgs(
-            files.bootClasspath,
-            files.dependencyClasspath,
             projectXml,
-            files.sourcePaths.files,
+            sourcePaths,
             outputLocation,
             generateApiMode,
             apiLintMode,
@@ -318,9 +315,7 @@
  * [GenerateApiMode.PublicApi].
  */
 fun getGenerateApiArgs(
-    bootClasspath: FileCollection,
-    dependencyClasspath: FileCollection,
-    projectXml: File?,
+    projectXml: File,
     sourcePaths: Collection<File>,
     outputLocation: ApiLocation?,
     generateApiMode: GenerateApiMode,
@@ -333,19 +328,10 @@
         mutableListOf(
             "--source-path",
             sourcePaths.filter { it.exists() }.joinToString(File.pathSeparator),
+            "--project",
+            projectXml.path
         )
 
-    // If there's a project xml file, the classpath isn't needed
-    args +=
-        if (projectXml != null) {
-            listOf("--project", projectXml.path)
-        } else {
-            listOf(
-                "--classpath",
-                (bootClasspath.files + dependencyClasspath.files).joinToString(File.pathSeparator)
-            )
-        }
-
     args += listOf("--format=v4", "--warnings-as-errors")
 
     pathToManifest?.let { args += listOf("--manifest", pathToManifest) }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTask.kt
index cda75c7..a84b92e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTask.kt
@@ -110,21 +110,22 @@
     @get:Input abstract val targetsJavaConsumers: Property<Boolean>
 
     /**
-     * Information about all source sets for multiplatform projects. Non-multiplatform projects can
-     * be represented as a list with one source set.
+     * Information about all source sets for multiplatform projects. Non-multiplatform projects
+     * should be represented as a list with one source set.
      *
      * This is marked as [Internal] because [compiledSources] is what should determine whether to
      * rerun metalava.
      */
-    @get:Internal abstract val optionalSourceSets: ListProperty<SourceSetInputs>
+    @get:Internal abstract val sourceSets: ListProperty<SourceSetInputs>
 
     /**
-     * Creates an XML file representing the project structure, if [optionalSourceSets] was set.
+     * Creates an XML file representing the project structure.
      *
      * This should only be called during task execution.
      */
-    protected fun createProjectXmlFile(): File? {
-        val sourceSets = optionalSourceSets.get().ifEmpty { null } ?: return null
+    protected fun createProjectXmlFile(): File {
+        val sourceSets = sourceSets.get()
+        check(sourceSets.isNotEmpty()) { "Project must have at least one source set." }
         val outputFile = File(temporaryDir, "project.xml")
         ProjectXml.create(
             sourceSets,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
index e5e949a..f0cea08 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
@@ -24,6 +24,7 @@
 import androidx.build.checkapi.ApiLocation
 import androidx.build.checkapi.CompilationInputs
 import androidx.build.checkapi.MultiplatformCompilationInputs
+import androidx.build.checkapi.SourceSetInputs
 import androidx.build.checkapi.getRequiredCompatibilityApiLocation
 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
 import androidx.build.version
@@ -218,7 +219,19 @@
         task.bootClasspath = inputs.bootClasspath
         androidManifest?.let { task.manifestPath.set(it) }
         if (inputs is MultiplatformCompilationInputs) {
-            task.optionalSourceSets.set(inputs.sourceSets)
+            task.sourceSets.set(inputs.sourceSets)
+        } else {
+            // Represent a non-multiplatform project as one source set.
+            task.sourceSets.set(
+                listOf(
+                    SourceSetInputs(
+                        sourceSetName = "main",
+                        dependsOnSourceSets = emptyList(),
+                        sourcePaths = inputs.sourcePaths,
+                        dependencyClasspath = inputs.dependencyClasspath
+                    )
+                )
+            )
         }
     }
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
index 351b606..617dc43 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
@@ -18,8 +18,7 @@
 
 import androidx.build.Version
 import androidx.build.checkapi.ApiLocation
-import androidx.build.checkapi.CompilationInputs
-import androidx.build.checkapi.StandardCompilationInputs
+import androidx.build.checkapi.SourceSetInputs
 import androidx.build.checkapi.getApiFileVersion
 import androidx.build.checkapi.getRequiredCompatibilityApiLocation
 import androidx.build.checkapi.getVersionedApiLocation
@@ -160,20 +159,27 @@
         outputApiLocation: ApiLocation,
     ) {
         val mavenId = "$groupId:$artifactId:$version"
-        val inputs: CompilationInputs?
-        try {
-            inputs = getFiles(runnerProject, mavenId)
-        } catch (e: TypedResolveException) {
-            runnerProject.logger.info("Ignoring missing artifact $mavenId: $e")
-            return
-        }
+        val (compiledSources, sourceSets) =
+            try {
+                getFiles(runnerProject, mavenId)
+            } catch (e: TypedResolveException) {
+                runnerProject.logger.info("Ignoring missing artifact $mavenId: $e")
+                return
+            }
 
         if (outputApiLocation.publicApiFile.exists()) {
             project.logger.lifecycle("Regenerating $mavenId")
+            val projectXml = File(temporaryDir, "$mavenId-project.xml")
+            ProjectXml.create(
+                sourceSets,
+                project.getAndroidJar().files,
+                compiledSources,
+                projectXml
+            )
             generateApi(
                 project.getMetalavaClasspath(),
-                null,
-                inputs,
+                projectXml,
+                sourceSets.flatMap { it.sourcePaths.files },
                 outputApiLocation,
                 ApiLintMode.Skip,
                 generateRestrictToLibraryGroupAPIs,
@@ -187,16 +193,32 @@
         }
     }
 
-    private fun getFiles(runnerProject: Project, mavenId: String): CompilationInputs {
+    /**
+     * For the given [mavenId], returns a pair with the source jar as the first element, and
+     * [SourceSetInputs] representing the unzipped sources as the second element.
+     */
+    private fun getFiles(
+        runnerProject: Project,
+        mavenId: String
+    ): Pair<File, List<SourceSetInputs>> {
         val jars = getJars(runnerProject, mavenId)
-        val sources = getSources(runnerProject, "$mavenId:sources")
+        val sourcesMavenId = "$mavenId:sources"
+        val compiledSources = getCompiledSources(runnerProject, sourcesMavenId)
+        val sources = getSources(runnerProject, sourcesMavenId, compiledSources)
 
         // TODO(b/330721660) parse META-INF/kotlin-project-structure-metadata.json for KMP projects
-        return StandardCompilationInputs(
-            sourcePaths = sources,
-            dependencyClasspath = jars,
-            bootClasspath = project.getAndroidJar()
-        )
+        // Represent the project as a single source set.
+        return compiledSources to
+            listOf(
+                SourceSetInputs(
+                    // Since there's just one source set, the name is arbitrary.
+                    sourceSetName = "main",
+                    // There are no other source sets to depend on.
+                    dependsOnSourceSets = emptyList(),
+                    sourcePaths = sources,
+                    dependencyClasspath = jars,
+                )
+            )
     }
 
     private fun getJars(runnerProject: Project, mavenId: String): FileCollection {
@@ -230,18 +252,27 @@
         return runnerProject.files()
     }
 
-    private fun getSources(runnerProject: Project, mavenId: String): FileCollection {
+    /** Returns the source jar for the [mavenId]. */
+    private fun getCompiledSources(runnerProject: Project, mavenId: String): File {
         val configuration =
             runnerProject.configurations.detachedConfiguration(
                 runnerProject.dependencies.create(mavenId)
             )
         configuration.isTransitive = false
+        return configuration.singleFile
+    }
 
+    /** Returns a file collection containing the unzipped sources from [compiledSources]. */
+    private fun getSources(
+        runnerProject: Project,
+        mavenId: String,
+        compiledSources: File
+    ): FileCollection {
         val sanitizedMavenId = mavenId.replace(":", "-")
         @Suppress("DEPRECATION")
         val unzippedDir = File("${runnerProject.buildDir.path}/sources-unzipped/$sanitizedMavenId")
         runnerProject.copy { copySpec ->
-            copySpec.from(runnerProject.zipTree(configuration.singleFile))
+            copySpec.from(runnerProject.zipTree(compiledSources))
             copySpec.into(unzippedDir)
         }
         return project.files(unzippedDir)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateBaselineTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateBaselineTasks.kt
index 3b6d269..fcc8eaa 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateBaselineTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateBaselineTasks.kt
@@ -55,8 +55,6 @@
         val baselineFile = baselines.get().apiLintFile
         val checkArgs =
             getGenerateApiArgs(
-                bootClasspath,
-                dependencyClasspath,
                 createProjectXmlFile(),
                 sourcePaths.files.filter { it.exists() },
                 null,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
index 2f84338..0c80536 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
@@ -96,9 +96,6 @@
         Debug.traceStart { "Create CameraPipe" }
         val timeSource = SystemTimeSource()
         val start = Timestamps.now(timeSource)
-        // Enable pruning device manager when tested in the MH lab.
-        val usePruningDeviceManager =
-            android.util.Log.isLoggable(CAMERA_PIPE_MH_FLAG, android.util.Log.DEBUG)
 
         val cameraPipe =
             CameraPipe(
@@ -110,16 +107,11 @@
                             sharedInteropCallbacks.sessionStateCallback,
                             openRetryMaxTimeout
                         ),
-                    usePruningDeviceManager = usePruningDeviceManager
+                    usePruningDeviceManager = true
                 )
             )
         Log.debug { "Created CameraPipe in ${start.measureNow(timeSource).formatMs()}" }
         Debug.traceStop()
         return cameraPipe
     }
-
-    private companion object {
-        // Flag set when being tested in the lab. Refer to CameraPipeConfigTestRule for more info.
-        const val CAMERA_PIPE_MH_FLAG = "CameraPipeMH"
-    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index 185aaed..5c817b5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -1231,12 +1231,10 @@
             when (targetAspectRatio[cameraMetadata, streamConfigurationMapCompat]) {
                 TargetAspectRatio.RATIO_4_3 -> AspectRatioUtil.ASPECT_RATIO_4_3
                 TargetAspectRatio.RATIO_16_9 -> AspectRatioUtil.ASPECT_RATIO_16_9
-                TargetAspectRatio.RATIO_MAX_JPEG -> {
-                    val maxJpegSize =
-                        getUpdatedSurfaceSizeDefinitionByFormat(ImageFormat.JPEG)
-                            .getMaximumSize(ImageFormat.JPEG)
-                    Rational(maxJpegSize.width, maxJpegSize.height)
-                }
+                TargetAspectRatio.RATIO_MAX_JPEG ->
+                    getUpdatedSurfaceSizeDefinitionByFormat(ImageFormat.JPEG)
+                        .getMaximumSize(ImageFormat.JPEG)
+                        ?.let { maxJpegSize -> Rational(maxJpegSize.width, maxJpegSize.height) }
                 else -> null
             }
         val resultList: MutableList<Size>
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 13a7539..464c132 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -897,6 +897,44 @@
             .isEqualTo(SurfaceConfig.create(ConfigType.JPEG, ConfigSize.ULTRA_MAXIMUM))
     }
 
+    @Test
+    fun transformSurfaceConfigWithUnsupportedFormatRecordSize() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+            supportedFormats =
+                intArrayOf(ImageFormat.YUV_420_888, ImageFormat.JPEG, ImageFormat.PRIVATE)
+        )
+        val supportedSurfaceCombination =
+            SupportedSurfaceCombination(context, fakeCameraMetadata, mockEncoderProfilesAdapter)
+        val surfaceConfig =
+            supportedSurfaceCombination.transformSurfaceConfig(
+                CameraMode.DEFAULT,
+                JPEG_R,
+                recordSize
+            )
+        val expectedSurfaceConfig = SurfaceConfig.create(ConfigType.JPEG_R, ConfigSize.RECORD)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithUnsupportedFormatMaximumSize() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+            supportedFormats =
+                intArrayOf(ImageFormat.YUV_420_888, ImageFormat.JPEG, ImageFormat.PRIVATE)
+        )
+        val supportedSurfaceCombination =
+            SupportedSurfaceCombination(context, fakeCameraMetadata, mockEncoderProfilesAdapter)
+        val surfaceConfig =
+            supportedSurfaceCombination.transformSurfaceConfig(
+                CameraMode.DEFAULT,
+                JPEG_R,
+                maximumSize
+            )
+        val expectedSurfaceConfig = SurfaceConfig.create(ConfigType.JPEG_R, ConfigSize.MAXIMUM)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
     // //////////////////////////////////////////////////////////////////////////////////////////
     //
     // Resolution selection tests for LEGACY-level guaranteed configurations
@@ -3349,7 +3387,14 @@
             characteristics
         )
 
-        whenever(mockMap.getOutputSizes(ArgumentMatchers.anyInt())).thenReturn(supportedSizes)
+        whenever(
+                mockMap.getOutputSizes(
+                    ArgumentMatchers.intThat { format ->
+                        supportedFormats?.contains(format) != false
+                    }
+                )
+            )
+            .thenReturn(supportedSizes)
         // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
         // output sizes need to be retrieved via SurfaceTexture.class.
         whenever(mockMap.getOutputSizes(SurfaceTexture::class.java)).thenReturn(supportedSizes)
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/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
index 3732cf5..96c7d56 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
@@ -83,7 +83,7 @@
 
     /**
      * A quirk that closes the camera devices before creating a new capture session. This is needed
-     * on legacy devices where creating a capture session directly may lead to deadlocks, NPEs or
+     * on certain devices where creating a capture session directly may lead to deadlocks, NPEs or
      * other undesirable behaviors. When [shouldCreateEmptyCaptureSessionBeforeClosing] is also
      * required, a regular camera device closure would then be expanded to:
      * 1. Close the camera device.
@@ -91,13 +91,20 @@
      * 3. Create an empty capture session.
      * 4. Close the capture session.
      * 5. Close the camera device.
-     * - Bug(s): b/237341513, b/359062845, b/342263275, b/379347826
+     * - Bug(s): b/237341513, b/359062845, b/342263275, b/379347826, b/359062845
      * - Device(s): Camera devices on hardware level LEGACY
      * - API levels: 23 (M) – 31 (S_V2)
      */
-    internal fun shouldCloseCameraBeforeCreatingCaptureSession(cameraId: CameraId): Boolean =
-        Build.VERSION.SDK_INT in (Build.VERSION_CODES.M..Build.VERSION_CODES.S_V2) &&
-            metadataProvider.awaitCameraMetadata(cameraId).isHardwareLevelLegacy
+    internal fun shouldCloseCameraBeforeCreatingCaptureSession(cameraId: CameraId): Boolean {
+        val isLegacyDevice =
+            Build.VERSION.SDK_INT in (Build.VERSION_CODES.M..Build.VERSION_CODES.S_V2) &&
+                metadataProvider.awaitCameraMetadata(cameraId).isHardwareLevelLegacy
+        val isQuirkyDevice =
+            "motorola".equals(Build.BRAND, ignoreCase = true) &&
+                "moto e20".equals(Build.MODEL, ignoreCase = true) &&
+                cameraId.value == "1"
+        return isLegacyDevice || isQuirkyDevice
+    }
 
     companion object {
         private val SHOULD_WAIT_FOR_REPEATING_DEVICE_MAP =
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/Camera2CameraControlImplDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
index ab6cf33..3191f91 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
@@ -455,7 +455,7 @@
         int defaultStrength = mCamera.getCameraInfo().getTorchStrengthLevel().getValue();
         // If the default strength is the max, set the strength to 1, otherwise, set to max.
         int customizedStrength = defaultStrength == maxStrength ? 1 : maxStrength;
-        camera2CameraControlImpl.setTorchStrengthLevelAsync(customizedStrength).get();
+        camera2CameraControlImpl.setTorchStrengthLevel(customizedStrength).get();
 
         // Assert: the customized strength is applied
         Camera2ImplConfig camera2Config = new Camera2ImplConfig(
@@ -483,7 +483,7 @@
 
         // Act & Assert
         try {
-            camera2CameraControlImpl.setTorchStrengthLevelAsync(0).get();
+            camera2CameraControlImpl.setTorchStrengthLevel(0).get();
         } catch (ExecutionException e) {
             assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
             return;
@@ -510,7 +510,7 @@
 
         // Act & Assert
         try {
-            camera2CameraControlImpl.setTorchStrengthLevelAsync(
+            camera2CameraControlImpl.setTorchStrengthLevel(
                     mCamera.getCameraInfo().getMaxTorchStrengthLevel() + 1).get();
         } catch (ExecutionException e) {
             assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
@@ -631,7 +631,7 @@
         int defaultStrength = mCamera.getCameraInfo().getTorchStrengthLevel().getValue();
         // If the default strength is the max, set the strength to 1, otherwise, set to max.
         int customizedStrength = defaultStrength == maxStrength ? 1 : maxStrength;
-        camera2CameraControlImpl.setTorchStrengthLevelAsync(customizedStrength).get();
+        camera2CameraControlImpl.setTorchStrengthLevel(customizedStrength).get();
 
         // Assert: the capture uses default torch strength
         CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
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..fa9b4f7 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
@@ -510,12 +510,16 @@
     }
 
     @Override
-    public @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+    public @NonNull ListenableFuture<Void> setTorchStrengthLevel(
             @IntRange(from = 1) int torchStrengthLevel) {
         if (!isControlInUse()) {
             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/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 4dc8635..af8059d5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -1131,7 +1131,8 @@
             case TargetAspectRatio.RATIO_MAX_JPEG:
                 Size maxJpegSize = getUpdatedSurfaceSizeDefinitionByFormat(
                         ImageFormat.JPEG).getMaximumSize(ImageFormat.JPEG);
-                ratio = new Rational(maxJpegSize.getWidth(), maxJpegSize.getHeight());
+                ratio = maxJpegSize == null ? null : new Rational(maxJpegSize.getWidth(),
+                        maxJpegSize.getHeight());
                 break;
             case TargetAspectRatio.RATIO_ORIGINAL:
                 ratio = null;
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/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 7f11ecb..eec2fef 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -1012,6 +1012,52 @@
             .isEqualTo(SurfaceConfig.create(ConfigType.JPEG, ConfigSize.ULTRA_MAXIMUM))
     }
 
+    @Test
+    fun transformSurfaceConfigWithUnsupportedFormatRecordSize() {
+        setupCameraAndInitCameraX(
+            supportedFormats =
+                intArrayOf(ImageFormat.YUV_420_888, ImageFormat.JPEG, ImageFormat.PRIVATE)
+        )
+        val supportedSurfaceCombination =
+            SupportedSurfaceCombination(
+                context,
+                DEFAULT_CAMERA_ID,
+                cameraManagerCompat!!,
+                mockCamcorderProfileHelper
+            )
+        val surfaceConfig =
+            supportedSurfaceCombination.transformSurfaceConfig(
+                CameraMode.DEFAULT,
+                JPEG_R,
+                RECORD_SIZE
+            )
+        val expectedSurfaceConfig = SurfaceConfig.create(ConfigType.JPEG_R, ConfigSize.RECORD)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithUnsupportedFormatMaximumSize() {
+        setupCameraAndInitCameraX(
+            supportedFormats =
+                intArrayOf(ImageFormat.YUV_420_888, ImageFormat.JPEG, ImageFormat.PRIVATE)
+        )
+        val supportedSurfaceCombination =
+            SupportedSurfaceCombination(
+                context,
+                DEFAULT_CAMERA_ID,
+                cameraManagerCompat!!,
+                mockCamcorderProfileHelper
+            )
+        val surfaceConfig =
+            supportedSurfaceCombination.transformSurfaceConfig(
+                CameraMode.DEFAULT,
+                JPEG_R,
+                MAXIMUM_SIZE
+            )
+        val expectedSurfaceConfig = SurfaceConfig.create(ConfigType.JPEG_R, ConfigSize.MAXIMUM)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
     // //////////////////////////////////////////////////////////////////////////////////////////
     //
     // Resolution selection tests for LEGACY-level guaranteed configurations
@@ -3634,7 +3680,15 @@
             Mockito.mock(StreamConfigurationMap::class.java).also { map ->
                 supportedSizes?.let {
                     // Sets up the supported sizes
-                    Mockito.`when`(map.getOutputSizes(anyInt())).thenReturn(it)
+                    Mockito.`when`(
+                            map.getOutputSizes(
+                                ArgumentMatchers.intThat { format ->
+                                    supportedFormats?.contains(format) != false
+                                }
+                            )
+                        )
+                        .thenReturn(it)
+
                     // ImageFormat.PRIVATE was supported since API level 23. Before that, the
                     // supported
                     // output sizes need to be retrieved via SurfaceTexture.class.
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 7837ece..002f57a 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -18,6 +18,7 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+    method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setTorchStrengthLevel(@IntRange(from=1) int);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
   }
@@ -48,11 +49,13 @@
     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=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);
     method public default java.util.Set<android.util.Range<java.lang.Integer!>!> getSupportedFrameRateRanges();
     method public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+    method public default androidx.lifecycle.LiveData<java.lang.Integer!> getTorchStrengthLevel();
     method public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
     method public boolean hasFlashUnit();
     method public default boolean isFocusMeteringSupported(androidx.camera.core.FocusMeteringAction);
@@ -61,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 {
@@ -255,15 +259,15 @@
   }
 
   public final class FlashState {
-    property public static final int FLASH_STATE_FIRED;
-    property public static final int FLASH_STATE_READY;
-    property public static final int FLASH_STATE_UNAVAILABLE;
-    property public static final int FLASH_STATE_UNKNOWN;
-    field public static final int FLASH_STATE_FIRED = 1; // 0x1
-    field public static final int FLASH_STATE_READY = 3; // 0x3
-    field public static final int FLASH_STATE_UNAVAILABLE = 2; // 0x2
-    field public static final int FLASH_STATE_UNKNOWN = 0; // 0x0
+    property public static final int FIRED;
+    property public static final int NOT_FIRED;
+    property public static final int UNAVAILABLE;
+    property public static final int UNKNOWN;
+    field public static final int FIRED = 1; // 0x1
     field public static final androidx.camera.core.FlashState INSTANCE;
+    field public static final int NOT_FIRED = 3; // 0x3
+    field public static final int UNAVAILABLE = 2; // 0x2
+    field public static final int UNKNOWN = 0; // 0x0
   }
 
   public final class FocusMeteringAction {
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 7837ece..002f57a 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -18,6 +18,7 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+    method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setTorchStrengthLevel(@IntRange(from=1) int);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
   }
@@ -48,11 +49,13 @@
     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=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);
     method public default java.util.Set<android.util.Range<java.lang.Integer!>!> getSupportedFrameRateRanges();
     method public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+    method public default androidx.lifecycle.LiveData<java.lang.Integer!> getTorchStrengthLevel();
     method public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
     method public boolean hasFlashUnit();
     method public default boolean isFocusMeteringSupported(androidx.camera.core.FocusMeteringAction);
@@ -61,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 {
@@ -255,15 +259,15 @@
   }
 
   public final class FlashState {
-    property public static final int FLASH_STATE_FIRED;
-    property public static final int FLASH_STATE_READY;
-    property public static final int FLASH_STATE_UNAVAILABLE;
-    property public static final int FLASH_STATE_UNKNOWN;
-    field public static final int FLASH_STATE_FIRED = 1; // 0x1
-    field public static final int FLASH_STATE_READY = 3; // 0x3
-    field public static final int FLASH_STATE_UNAVAILABLE = 2; // 0x2
-    field public static final int FLASH_STATE_UNKNOWN = 0; // 0x0
+    property public static final int FIRED;
+    property public static final int NOT_FIRED;
+    property public static final int UNAVAILABLE;
+    property public static final int UNKNOWN;
+    field public static final int FIRED = 1; // 0x1
     field public static final androidx.camera.core.FlashState INSTANCE;
+    field public static final int NOT_FIRED = 3; // 0x3
+    field public static final int UNAVAILABLE = 2; // 0x2
+    field public static final int UNKNOWN = 0; // 0x0
   }
 
   public final class FocusMeteringAction {
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ImageUtilDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ImageUtilDeviceTest.kt
index efa93b9..96d10ce 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ImageUtilDeviceTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ImageUtilDeviceTest.kt
@@ -124,7 +124,7 @@
                     0,
                     0,
                     Matrix(),
-                    FlashState.FLASH_STATE_UNKNOWN
+                    FlashState.UNKNOWN
                 ),
                 WIDTH,
                 HEIGHT
@@ -146,7 +146,7 @@
                     0,
                     0,
                     Matrix(),
-                    FlashState.FLASH_STATE_UNKNOWN
+                    FlashState.UNKNOWN
                 ),
                 WIDTH,
                 HEIGHT
@@ -228,7 +228,7 @@
                     0,
                     0,
                     Matrix(),
-                    FlashState.FLASH_STATE_UNKNOWN
+                    FlashState.UNKNOWN
                 )
             )
         image.format = ImageFormat.PRIVATE
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageProxy.java
index 3e05248..326b4ac 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageProxy.java
@@ -58,7 +58,7 @@
                 image.getTimestamp(),
                 0,
                 new Matrix(),
-                FlashState.FLASH_STATE_UNKNOWN);
+                FlashState.UNKNOWN);
     }
 
     @Override
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 09718fb..d0ab733 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,12 +245,16 @@
      * {@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.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    default @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+    @SuppressWarnings("AsyncSuffixFuture")
+    default @NonNull ListenableFuture<Void> setTorchStrengthLevel(
             @IntRange(from = 1) int torchStrengthLevel) {
         return Futures.immediateFailedFuture(new UnsupportedOperationException(
                 "Setting torch strength is not supported on the device."));
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 91b2f00..a952344 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,24 +435,25 @@
     /**
      * Returns the maximum torch strength level.
      *
-     * @return The maximum strength level. If the device doesn't support configuring torch
-     * strength, returns {@code 1}.
+     * @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.
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    @IntRange(from = 1)
+    @IntRange(from = 0)
     default int getMaxTorchStrengthLevel() {
-        return 1;
+        return TORCH_STRENGTH_LEVEL_UNSUPPORTED;
     }
 
     /**
      * Returns the {@link LiveData} of the torch strength level.
      *
      * <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.
+     * device if {@link CameraControl#setTorchStrengthLevel(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.
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
     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/FlashState.kt b/camera/camera-core/src/main/java/androidx/camera/core/FlashState.kt
index 21b56c6..c042355 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/FlashState.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/FlashState.kt
@@ -23,8 +23,7 @@
 /**
  * The camera flash state values represent the state of the physical flash unit of a camera.
  *
- * The possible values are [FLASH_STATE_UNKNOWN], [FLASH_STATE_FIRED], [FLASH_STATE_UNAVAILABLE],
- * and [FLASH_STATE_READY].
+ * The possible values are [UNKNOWN], [FIRED], [UNAVAILABLE], and [NOT_FIRED].
  *
  * In case of any error, how it is notified depends on the API that is used for obtaining the
  * [FlashState]. For example, if the flash state is obtained by invoking
@@ -34,7 +33,7 @@
  */
 public object FlashState {
     /** The camera flash state. */
-    @IntDef(FLASH_STATE_UNKNOWN, FLASH_STATE_FIRED, FLASH_STATE_UNAVAILABLE, FLASH_STATE_READY)
+    @IntDef(UNKNOWN, FIRED, UNAVAILABLE, NOT_FIRED)
     @Retention(AnnotationRetention.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public annotation class FlashState
@@ -47,7 +46,7 @@
      *
      * @see FlashState
      */
-    public const val FLASH_STATE_UNKNOWN: Int = 0
+    public const val UNKNOWN: Int = 0
 
     /**
      * State indicating the flash was fired.
@@ -59,7 +58,7 @@
      *
      * @see FlashState
      */
-    public const val FLASH_STATE_FIRED: Int = 1
+    public const val FIRED: Int = 1
 
     /**
      * State indicating that flash is unavailable.
@@ -69,14 +68,14 @@
      *
      * @see FlashState
      */
-    public const val FLASH_STATE_UNAVAILABLE: Int = 2
+    public const val UNAVAILABLE: Int = 2
 
     /**
-     * State indicating that flash is ready and can be fired if required.
+     * State indicating that flash has not been fired.
      *
-     * This is used when the flash is available to be fired but not fired yet.
+     * This is used when the flash is available but has not been fired.
      *
      * @see FlashState
      */
-    public const val FLASH_STATE_READY: Int = 3
+    public const val NOT_FIRED: Int = 3
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
index e14a903..40c25a6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
@@ -17,7 +17,7 @@
 package androidx.camera.core;
 
 import static androidx.camera.core.FlashState.FlashState;
-import static androidx.camera.core.FlashState.FLASH_STATE_UNKNOWN;
+import static androidx.camera.core.FlashState.UNKNOWN;
 
 import android.graphics.Matrix;
 import android.hardware.camera2.CameraCharacteristics;
@@ -124,7 +124,7 @@
      */
     @FlashState
     default int getFlashState() {
-        return FLASH_STATE_UNKNOWN;
+        return UNKNOWN;
     }
 
     /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureMetaData.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureMetaData.java
index e72e824..6cd4f2e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureMetaData.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureMetaData.java
@@ -17,10 +17,8 @@
 package androidx.camera.core.impl;
 
 
-import static androidx.camera.core.FlashState.FLASH_STATE_FIRED;
-import static androidx.camera.core.FlashState.FLASH_STATE_READY;
-import static androidx.camera.core.FlashState.FLASH_STATE_UNAVAILABLE;
-import static androidx.camera.core.FlashState.FLASH_STATE_UNKNOWN;
+import static androidx.camera.core.FlashState.NOT_FIRED;
+import static androidx.camera.core.FlashState.UNAVAILABLE;
 
 /**
  * This class defines the enumeration constants used for querying the camera capture mode and
@@ -170,13 +168,13 @@
         public @androidx.camera.core.FlashState.FlashState int toFlashState() {
             switch (this) {
                 case NONE:
-                    return FLASH_STATE_UNAVAILABLE;
+                    return UNAVAILABLE;
                 case READY:
-                    return FLASH_STATE_READY;
+                    return NOT_FIRED;
                 case FIRED:
-                    return FLASH_STATE_FIRED;
+                    return androidx.camera.core.FlashState.FIRED;
                 default:
-                    return FLASH_STATE_UNKNOWN;
+                    return androidx.camera.core.FlashState.UNKNOWN;
             }
         }
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
index b39ebca..f18d111 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
@@ -84,9 +84,9 @@
     }
 
     @Override
-    public @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+    public @NonNull ListenableFuture<Void> setTorchStrengthLevel(
             @IntRange(from = 1) int torchStrengthLevel) {
-        return mCameraControlInternal.setTorchStrengthLevelAsync(torchStrengthLevel);
+        return mCameraControlInternal.setTorchStrengthLevel(torchStrengthLevel);
     }
 
     @Override
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-core/src/main/java/androidx/camera/core/impl/SurfaceConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SurfaceConfig.java
index 0bf64c9..2b32965 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SurfaceConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SurfaceConfig.java
@@ -16,6 +16,8 @@
 
 package androidx.camera.core.impl;
 
+import static androidx.camera.core.impl.CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA;
+
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCaptureSession.StateCallback;
 import android.os.Handler;
@@ -158,12 +160,18 @@
                 configSize = ConfigSize.PREVIEW;
             } else if (sizeArea <= SizeUtil.getArea(surfaceSizeDefinition.getRecordSize())) {
                 configSize = ConfigSize.RECORD;
-            } else if (sizeArea <= SizeUtil.getArea(
-                    surfaceSizeDefinition.getMaximumSize(imageFormat))) {
-                configSize = ConfigSize.MAXIMUM;
             } else {
+                Size maximumSize = surfaceSizeDefinition.getMaximumSize(imageFormat);
                 Size ultraMaximumSize = surfaceSizeDefinition.getUltraMaximumSize(imageFormat);
-                if (ultraMaximumSize != null && sizeArea <= SizeUtil.getArea(ultraMaximumSize)) {
+                // On some devices, when extensions is on, some extra formats might be supported
+                // for extensions. But those formats are not supported in the normal mode. In
+                // that case, MaximumSize could be null. Directly make configSize as MAXIMUM for
+                // the case.
+                if ((maximumSize == null || sizeArea <= SizeUtil.getArea(maximumSize))
+                        && cameraMode != ULTRA_HIGH_RESOLUTION_CAMERA) {
+                    configSize = ConfigSize.MAXIMUM;
+                } else if (ultraMaximumSize != null && sizeArea <= SizeUtil.getArea(
+                        ultraMaximumSize)) {
                     configSize = ConfigSize.ULTRA_MAXIMUM;
                 }
             }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SurfaceSizeDefinition.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SurfaceSizeDefinition.java
index 4e3c5f6..1bb17b0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SurfaceSizeDefinition.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SurfaceSizeDefinition.java
@@ -125,7 +125,7 @@
      * Returns the MAXIMUM size for the specified format, or {@code null} null if there is no
      * data for the format.
      */
-    public @NonNull Size getMaximumSize(int format) {
+    public @Nullable Size getMaximumSize(int format) {
         return getMaximumSizeMap().get(format);
     }
 
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/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/util/CaptureUtils.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/util/CaptureUtils.kt
index b5e8349..35d0117 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/util/CaptureUtils.kt
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/util/CaptureUtils.kt
@@ -104,9 +104,9 @@
 
         val flashState =
             when (image.imageInfo.flashState) {
-                FlashState.FLASH_STATE_FIRED -> "FIRED"
-                FlashState.FLASH_STATE_UNAVAILABLE -> "UNAVAILABLE"
-                FlashState.FLASH_STATE_READY -> "READY"
+                FlashState.FIRED -> "FIRED"
+                FlashState.UNAVAILABLE -> "UNAVAILABLE"
+                FlashState.NOT_FIRED -> "NOT_FIRED"
                 else -> "UNKNOWN"
             }
         Toast.makeText(context, "Flash state: $flashState", Toast.LENGTH_SHORT).show()
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/animation/animation-core/proguard-rules.pro b/compose/animation/animation-core/proguard-rules.pro
index 72f4a6c..67d118b 100644
--- a/compose/animation/animation-core/proguard-rules.pro
+++ b/compose/animation/animation-core/proguard-rules.pro
@@ -15,7 +15,7 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.**.* {
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
     static void throw*Exception(...);
     static void throw*ExceptionForNullCheck(...);
     # For methods returning Nothing
diff --git a/compose/foundation/foundation-layout/proguard-rules.pro b/compose/foundation/foundation-layout/proguard-rules.pro
index 72f4a6c..67d118b 100644
--- a/compose/foundation/foundation-layout/proguard-rules.pro
+++ b/compose/foundation/foundation-layout/proguard-rules.pro
@@ -15,7 +15,7 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.**.* {
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
     static void throw*Exception(...);
     static void throw*ExceptionForNullCheck(...);
     # For methods returning Nothing
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AlignmentLine.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AlignmentLine.kt
index 99d2381..fb9ac8c05 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AlignmentLine.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AlignmentLine.kt
@@ -33,7 +33,6 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.isSpecified
 import androidx.compose.ui.unit.isUnspecified
 import kotlin.math.max
 
@@ -143,14 +142,14 @@
 @Stable
 fun Modifier.paddingFromBaseline(top: Dp = Dp.Unspecified, bottom: Dp = Dp.Unspecified) =
     this.then(
-            if (top.isSpecified) {
+            if (top != Dp.Unspecified) {
                 Modifier.paddingFrom(FirstBaseline, before = top)
             } else {
                 Modifier
             }
         )
         .then(
-            if (bottom.isSpecified) {
+            if (bottom != Dp.Unspecified) {
                 Modifier.paddingFrom(LastBaseline, after = bottom)
             } else {
                 Modifier
@@ -193,8 +192,8 @@
 ) : ModifierNodeElement<AlignmentLineOffsetDpNode>() {
     init {
         requirePrecondition(
-            (before.value >= 0f || before.isUnspecified) and
-                (after.value >= 0f || after.isUnspecified)
+            (before.value >= 0f || before == Dp.Unspecified) &&
+                (after.value >= 0f || after == Dp.Unspecified)
         ) {
             "Padding from alignment line must be a non-negative number"
         }
@@ -320,12 +319,12 @@
     val axisMax = if (alignmentLine.horizontal) constraints.maxHeight else constraints.maxWidth
     // Compute padding required to satisfy the total before and after offsets.
     val paddingBefore =
-        ((if (before.isSpecified) before.roundToPx() else 0) - linePosition).coerceIn(
+        ((if (before != Dp.Unspecified) before.roundToPx() else 0) - linePosition).coerceIn(
             0,
             axisMax - axis
         )
     val paddingAfter =
-        ((if (after.isSpecified) after.roundToPx() else 0) - axis + linePosition).coerceIn(
+        ((if (after != Dp.Unspecified) after.roundToPx() else 0) - axis + linePosition).coerceIn(
             0,
             axisMax - axis - paddingBefore
         )
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
index c038279..24ac686 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
@@ -33,7 +33,6 @@
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.isUnspecified
 import androidx.compose.ui.unit.offset
 
 /**
@@ -204,14 +203,10 @@
     ) : PaddingValues {
 
         init {
-            requirePrecondition(
-                (left.value >= 0f) and
-                    (top.value >= 0f) and
-                    (right.value >= 0f) and
-                    (bottom.value >= 0f)
-            ) {
-                "Padding must be non-negative"
-            }
+            requirePrecondition(left.value >= 0) { "Left padding must be non-negative" }
+            requirePrecondition(top.value >= 0) { "Top padding must be non-negative" }
+            requirePrecondition(right.value >= 0) { "Right padding must be non-negative" }
+            requirePrecondition(bottom.value >= 0) { "Bottom padding must be non-negative" }
         }
 
         override fun calculateLeftPadding(layoutDirection: LayoutDirection) = left
@@ -296,11 +291,10 @@
 ) : PaddingValues {
 
     init {
-        requirePrecondition(
-            (start.value >= 0f) and (top.value >= 0f) and (end.value >= 0f) and (bottom.value >= 0f)
-        ) {
-            "Padding must be non-negative"
-        }
+        requirePrecondition(start.value >= 0) { "Start padding must be non-negative" }
+        requirePrecondition(top.value >= 0) { "Top padding must be non-negative" }
+        requirePrecondition(end.value >= 0) { "End padding must be non-negative" }
+        requirePrecondition(bottom.value >= 0) { "Bottom padding must be non-negative" }
     }
 
     override fun calculateLeftPadding(layoutDirection: LayoutDirection) =
@@ -338,10 +332,10 @@
 
     init {
         requirePrecondition(
-            (start.value >= 0f || start.isUnspecified) and
-                (top.value >= 0f || top.isUnspecified) and
-                (end.value >= 0f || end.isUnspecified) and
-                (bottom.value >= 0f || bottom.isUnspecified)
+            (start.value >= 0f || start == Dp.Unspecified) &&
+                (top.value >= 0f || top == Dp.Unspecified) &&
+                (end.value >= 0f || end == Dp.Unspecified) &&
+                (bottom.value >= 0f || bottom == Dp.Unspecified)
         ) {
             "Padding must be non-negative"
         }
@@ -442,30 +436,30 @@
         measurable: Measurable,
         constraints: Constraints
     ): MeasureResult {
-        val leftPadding = paddingValues.calculateLeftPadding(layoutDirection)
-        val topPadding = paddingValues.calculateTopPadding()
-        val rightPadding = paddingValues.calculateRightPadding(layoutDirection)
-        val bottomPadding = paddingValues.calculateBottomPadding()
-
         requirePrecondition(
-            (leftPadding >= 0.dp) and
-                (topPadding >= 0.dp) and
-                (rightPadding >= 0.dp) and
-                (bottomPadding >= 0.dp)
+            paddingValues.calculateLeftPadding(layoutDirection) >= 0.dp &&
+                paddingValues.calculateTopPadding() >= 0.dp &&
+                paddingValues.calculateRightPadding(layoutDirection) >= 0.dp &&
+                paddingValues.calculateBottomPadding() >= 0.dp
         ) {
             "Padding must be non-negative"
         }
-
-        val roundedLeftPadding = leftPadding.roundToPx()
-        val horizontal = roundedLeftPadding + rightPadding.roundToPx()
-
-        val roundedTopPadding = topPadding.roundToPx()
-        val vertical = roundedTopPadding + bottomPadding.roundToPx()
+        val horizontal =
+            paddingValues.calculateLeftPadding(layoutDirection).roundToPx() +
+                paddingValues.calculateRightPadding(layoutDirection).roundToPx()
+        val vertical =
+            paddingValues.calculateTopPadding().roundToPx() +
+                paddingValues.calculateBottomPadding().roundToPx()
 
         val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
 
         val width = constraints.constrainWidth(placeable.width + horizontal)
         val height = constraints.constrainHeight(placeable.height + vertical)
-        return layout(width, height) { placeable.place(roundedLeftPadding, roundedTopPadding) }
+        return layout(width, height) {
+            placeable.place(
+                paddingValues.calculateLeftPadding(layoutDirection).roundToPx(),
+                paddingValues.calculateTopPadding().roundToPx()
+            )
+        }
     }
 }
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
index 52e00de..b221c8b 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
@@ -39,10 +39,6 @@
 import androidx.compose.ui.unit.constrain
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.isSpecified
-import androidx.compose.ui.util.fastCoerceAtLeast
-import androidx.compose.ui.util.fastCoerceAtMost
-import androidx.compose.ui.util.fastCoerceIn
 import androidx.compose.ui.util.fastRoundToInt
 
 /**
@@ -698,7 +694,7 @@
             val width =
                 (constraints.maxWidth * fraction)
                     .fastRoundToInt()
-                    .fastCoerceIn(constraints.minWidth, constraints.maxWidth)
+                    .coerceIn(constraints.minWidth, constraints.maxWidth)
             minWidth = width
             maxWidth = width
         } else {
@@ -711,7 +707,7 @@
             val height =
                 (constraints.maxHeight * fraction)
                     .fastRoundToInt()
-                    .fastCoerceIn(constraints.minHeight, constraints.maxHeight)
+                    .coerceIn(constraints.minHeight, constraints.maxHeight)
             minHeight = height
             maxHeight = height
         } else {
@@ -786,28 +782,28 @@
     private val Density.targetConstraints: Constraints
         get() {
             val maxWidth =
-                if (maxWidth.isSpecified) {
-                    maxWidth.roundToPx().fastCoerceAtLeast(0)
+                if (maxWidth != Dp.Unspecified) {
+                    maxWidth.roundToPx().coerceAtLeast(0)
                 } else {
                     Constraints.Infinity
                 }
             val maxHeight =
-                if (maxHeight.isSpecified) {
-                    maxHeight.roundToPx().fastCoerceAtLeast(0)
+                if (maxHeight != Dp.Unspecified) {
+                    maxHeight.roundToPx().coerceAtLeast(0)
                 } else {
                     Constraints.Infinity
                 }
             val minWidth =
-                if (minWidth.isSpecified) {
-                    minWidth.roundToPx().fastCoerceIn(0, maxWidth).let {
+                if (minWidth != Dp.Unspecified) {
+                    minWidth.roundToPx().coerceAtMost(maxWidth).coerceAtLeast(0).let {
                         if (it != Constraints.Infinity) it else 0
                     }
                 } else {
                     0
                 }
             val minHeight =
-                if (minHeight.isSpecified) {
-                    minHeight.roundToPx().fastCoerceIn(0, maxHeight).let {
+                if (minHeight != Dp.Unspecified) {
+                    minHeight.roundToPx().coerceAtMost(maxHeight).coerceAtLeast(0).let {
                         if (it != Constraints.Infinity) it else 0
                     }
                 } else {
@@ -831,28 +827,28 @@
                     constraints.constrain(targetConstraints)
                 } else {
                     val resolvedMinWidth =
-                        if (minWidth.isSpecified) {
+                        if (minWidth != Dp.Unspecified) {
                             targetConstraints.minWidth
                         } else {
-                            constraints.minWidth.fastCoerceAtMost(targetConstraints.maxWidth)
+                            constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
                         }
                     val resolvedMaxWidth =
-                        if (maxWidth.isSpecified) {
+                        if (maxWidth != Dp.Unspecified) {
                             targetConstraints.maxWidth
                         } else {
-                            constraints.maxWidth.fastCoerceAtLeast(targetConstraints.minWidth)
+                            constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
                         }
                     val resolvedMinHeight =
-                        if (minHeight.isSpecified) {
+                        if (minHeight != Dp.Unspecified) {
                             targetConstraints.minHeight
                         } else {
-                            constraints.minHeight.fastCoerceAtMost(targetConstraints.maxHeight)
+                            constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
                         }
                     val resolvedMaxHeight =
-                        if (maxHeight.isSpecified) {
+                        if (maxHeight != Dp.Unspecified) {
                             targetConstraints.maxHeight
                         } else {
-                            constraints.maxHeight.fastCoerceAtLeast(targetConstraints.minHeight)
+                            constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
                         }
                     Constraints(
                         resolvedMinWidth,
@@ -1076,14 +1072,14 @@
     ): MeasureResult {
         val wrappedConstraints =
             Constraints(
-                if (minWidth.isSpecified && constraints.minWidth == 0) {
-                    minWidth.roundToPx().fastCoerceIn(0, constraints.maxWidth)
+                if (minWidth != Dp.Unspecified && constraints.minWidth == 0) {
+                    minWidth.roundToPx().coerceAtMost(constraints.maxWidth).coerceAtLeast(0)
                 } else {
                     constraints.minWidth
                 },
                 constraints.maxWidth,
-                if (minHeight.isSpecified && constraints.minHeight == 0) {
-                    minHeight.roundToPx().fastCoerceIn(0, constraints.maxHeight)
+                if (minHeight != Dp.Unspecified && constraints.minHeight == 0) {
+                    minHeight.roundToPx().coerceAtMost(constraints.maxHeight).coerceAtLeast(0)
                 } else {
                     constraints.minHeight
                 },
@@ -1099,7 +1095,7 @@
     ) =
         measurable
             .minIntrinsicWidth(height)
-            .fastCoerceAtLeast(if (minWidth.isSpecified) minWidth.roundToPx() else 0)
+            .coerceAtLeast(if (minWidth != Dp.Unspecified) minWidth.roundToPx() else 0)
 
     override fun IntrinsicMeasureScope.maxIntrinsicWidth(
         measurable: IntrinsicMeasurable,
@@ -1107,7 +1103,7 @@
     ) =
         measurable
             .maxIntrinsicWidth(height)
-            .fastCoerceAtLeast(if (minWidth.isSpecified) minWidth.roundToPx() else 0)
+            .coerceAtLeast(if (minWidth != Dp.Unspecified) minWidth.roundToPx() else 0)
 
     override fun IntrinsicMeasureScope.minIntrinsicHeight(
         measurable: IntrinsicMeasurable,
@@ -1115,7 +1111,7 @@
     ) =
         measurable
             .minIntrinsicHeight(width)
-            .fastCoerceAtLeast(if (minHeight.isSpecified) minHeight.roundToPx() else 0)
+            .coerceAtLeast(if (minHeight != Dp.Unspecified) minHeight.roundToPx() else 0)
 
     override fun IntrinsicMeasureScope.maxIntrinsicHeight(
         measurable: IntrinsicMeasurable,
@@ -1123,7 +1119,7 @@
     ) =
         measurable
             .maxIntrinsicHeight(width)
-            .fastCoerceAtLeast(if (minHeight.isSpecified) minHeight.roundToPx() else 0)
+            .coerceAtLeast(if (minHeight != Dp.Unspecified) minHeight.roundToPx() else 0)
 }
 
 internal enum class Direction {
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 91a0069..0d84d31 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -50,7 +50,6 @@
                 implementation(project(":compose:ui:ui-text"))
                 implementation(project(":compose:ui:ui-util"))
                 implementation(project(":compose:foundation:foundation-layout"))
-                implementation(project(":performance:performance-annotation"))
             }
         }
 
@@ -81,16 +80,12 @@
             dependsOn(commonMain)
         }
 
-        nonAndroidStubsMain {
+        jvmStubsMain {
             dependsOn(commonStubsMain)
         }
 
-        jvmStubsMain {
-            dependsOn(nonAndroidStubsMain)
-        }
-
         linuxx64StubsMain {
-            dependsOn(nonAndroidStubsMain)
+            dependsOn(commonStubsMain)
         }
 
         androidInstrumentedTest {
diff --git a/compose/foundation/foundation/proguard-rules.pro b/compose/foundation/foundation/proguard-rules.pro
index 2d14261..d07663a 100644
--- a/compose/foundation/foundation/proguard-rules.pro
+++ b/compose/foundation/foundation/proguard-rules.pro
@@ -15,12 +15,8 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.**.* {
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
     static void throw*Exception(...);
     # For methods returning Nothing
     static java.lang.Void throw*Exception(...);
 }
-
--keepclassmembers class * {
-    @dalvik.annotation.optimization.NeverInline *;
-}
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/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
index bc90119..9757d4a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
@@ -139,6 +139,14 @@
     }
 
     @Test
+    fun textField_newLineNumpad() {
+        keysSequenceTest(initText = "hello") {
+            Key.NumPadEnter.downAndUp()
+            expectedText("\nhello")
+        }
+    }
+
+    @Test
     fun textField_backspace() {
         keysSequenceTest(initText = "hello") {
             Key.DirectionRight.downAndUp()
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/KeyMapping.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/KeyMapping.android.kt
index d22ae99..52f59db 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/KeyMapping.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/KeyMapping.android.kt
@@ -66,6 +66,7 @@
     actual val MoveEnd: Key = Key(AndroidKeyEvent.KEYCODE_MOVE_END)
     actual val Insert: Key = Key(AndroidKeyEvent.KEYCODE_INSERT)
     actual val Enter: Key = Key(AndroidKeyEvent.KEYCODE_ENTER)
+    actual val NumPadEnter: Key = Key(AndroidKeyEvent.KEYCODE_NUMPAD_ENTER)
     actual val Backspace: Key = Key(AndroidKeyEvent.KEYCODE_DEL)
     actual val Delete: Key = Key(AndroidKeyEvent.KEYCODE_FORWARD_DEL)
     actual val Paste: Key = Key(AndroidKeyEvent.KEYCODE_PASTE)
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/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 7c73fa8..7a034224 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -443,28 +443,44 @@
         measurable: IntrinsicMeasurable,
         height: Int
     ): Int {
-        return measurable.minIntrinsicWidth(if (isVertical) Constraints.Infinity else height)
+        return if (isVertical) {
+            measurable.minIntrinsicWidth(Constraints.Infinity)
+        } else {
+            measurable.minIntrinsicWidth(height)
+        }
     }
 
     override fun IntrinsicMeasureScope.minIntrinsicHeight(
         measurable: IntrinsicMeasurable,
         width: Int
     ): Int {
-        return measurable.minIntrinsicHeight(if (isVertical) width else Constraints.Infinity)
+        return if (isVertical) {
+            measurable.minIntrinsicHeight(width)
+        } else {
+            measurable.minIntrinsicHeight(Constraints.Infinity)
+        }
     }
 
     override fun IntrinsicMeasureScope.maxIntrinsicWidth(
         measurable: IntrinsicMeasurable,
         height: Int
     ): Int {
-        return measurable.maxIntrinsicWidth(if (isVertical) Constraints.Infinity else height)
+        return if (isVertical) {
+            measurable.maxIntrinsicWidth(Constraints.Infinity)
+        } else {
+            measurable.maxIntrinsicWidth(height)
+        }
     }
 
     override fun IntrinsicMeasureScope.maxIntrinsicHeight(
         measurable: IntrinsicMeasurable,
         width: Int
     ): Int {
-        return measurable.maxIntrinsicHeight(if (isVertical) width else Constraints.Infinity)
+        return if (isVertical) {
+            measurable.maxIntrinsicHeight(width)
+        } else {
+            measurable.maxIntrinsicHeight(Constraints.Infinity)
+        }
     }
 
     override fun SemanticsPropertyReceiver.applySemantics() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index db0c790..dd3ef0b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -62,7 +62,6 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
-import dalvik.annotation.optimization.NeverInline
 import kotlin.math.abs
 import kotlin.math.max
 import kotlin.math.min
@@ -633,7 +632,6 @@
         positions[keys.size - 1] = position
     }
 
-    @NeverInline
     internal fun buildPositions(): FloatArray {
         // We might have expanded more than we actually need, so trim the array
         return positions.copyOfRange(
@@ -645,7 +643,6 @@
 
     internal fun buildKeys(): List<T> = keys
 
-    @NeverInline
     private fun expandPositions() {
         positions = positions.copyOf(keys.size + 2)
     }
@@ -1653,7 +1650,7 @@
     }
 }
 
-internal expect inline fun assertOnJvm(statement: Boolean, message: () -> String)
+internal expect inline fun assertOnJvm(statement: Boolean, message: () -> String): Unit
 
 internal val AnchoredDraggableMinFlingVelocity = 125.dp
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/internal/InlineClassHelper.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/internal/InlineClassHelper.kt
index 686d885..4af862a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/internal/InlineClassHelper.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/internal/InlineClassHelper.kt
@@ -53,7 +53,7 @@
     }
 }
 
-@Suppress("NOTHING_TO_INLINE", "BanInlineOptIn", "KotlinRedundantDiagnosticSuppress")
+@Suppress("NOTHING_TO_INLINE", "BanInlineOptIn")
 @OptIn(ExperimentalContracts::class)
 internal inline fun checkPrecondition(value: Boolean) {
     contract { returns() implies value }
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/KeyMapping.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
index 153b5da..2764e2e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
@@ -51,6 +51,7 @@
     val MoveEnd: Key
     val Insert: Key
     val Enter: Key
+    val NumPadEnter: Key
     val Backspace: Key
     val Delete: Key
     val Paste: Key
@@ -104,7 +105,8 @@
                         MappedKeys.PageDown -> KeyCommand.PAGE_DOWN
                         MappedKeys.MoveHome -> KeyCommand.LINE_START
                         MappedKeys.MoveEnd -> KeyCommand.LINE_END
-                        MappedKeys.Enter -> KeyCommand.NEW_LINE
+                        MappedKeys.Enter,
+                        MappedKeys.NumPadEnter -> KeyCommand.NEW_LINE
                         MappedKeys.Backspace -> KeyCommand.DELETE_PREV_CHAR
                         MappedKeys.Delete -> KeyCommand.DELETE_NEXT_CHAR
                         MappedKeys.Paste -> KeyCommand.PASTE
@@ -146,8 +148,8 @@
                         }
                     event.isShiftPressed ->
                         when (event.key) {
-                            MappedKeys.MoveHome -> KeyCommand.SELECT_LINE_LEFT
-                            MappedKeys.MoveEnd -> KeyCommand.SELECT_LINE_RIGHT
+                            MappedKeys.MoveHome -> KeyCommand.SELECT_LINE_START
+                            MappedKeys.MoveEnd -> KeyCommand.SELECT_LINE_END
                             else -> null
                         }
                     event.isAltPressed ->
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/OffsetMappingCalculator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/OffsetMappingCalculator.kt
index eaa2856..b6fb607 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/OffsetMappingCalculator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/OffsetMappingCalculator.kt
@@ -370,18 +370,18 @@
  */
 @kotlin.jvm.JvmInline
 private value class OpArray private constructor(private val values: IntArray) {
-    constructor(size: Int) : this(IntArray(size * OpArrayElementSize))
+    constructor(size: Int) : this(IntArray(size * ElementSize))
 
     val size: Int
-        get() = values.size / OpArrayElementSize
+        get() = values.size / ElementSize
 
     fun set(index: Int, offset: Int, srcLen: Int, destLen: Int) {
-        values[index * OpArrayElementSize] = offset
-        values[index * OpArrayElementSize + 1] = srcLen
-        values[index * OpArrayElementSize + 2] = destLen
+        values[index * ElementSize] = offset
+        values[index * ElementSize + 1] = srcLen
+        values[index * ElementSize + 2] = destLen
     }
 
-    fun copyOf(newSize: Int) = OpArray(values.copyOf(newSize * OpArrayElementSize))
+    fun copyOf(newSize: Int) = OpArray(values.copyOf(newSize * ElementSize))
 
     /**
      * Loops through the array between 0 and [max] (exclusive). If [reversed] is false (the
@@ -399,20 +399,22 @@
         // duplication here keeps the more complicated logic at the callsite more readable.
         if (reversed) {
             for (i in max - 1 downTo 0) {
-                val offset = values[i * OpArrayElementSize]
-                val srcLen = values[i * OpArrayElementSize + 1]
-                val destLen = values[i * OpArrayElementSize + 2]
+                val offset = values[i * ElementSize]
+                val srcLen = values[i * ElementSize + 1]
+                val destLen = values[i * ElementSize + 2]
                 block(offset, srcLen, destLen)
             }
         } else {
             for (i in 0 until max) {
-                val offset = values[i * OpArrayElementSize]
-                val srcLen = values[i * OpArrayElementSize + 1]
-                val destLen = values[i * OpArrayElementSize + 2]
+                val offset = values[i * ElementSize]
+                val srcLen = values[i * ElementSize + 1]
+                val destLen = values[i * ElementSize + 2]
                 block(offset, srcLen, destLen)
             }
         }
     }
-}
 
-private const val OpArrayElementSize = 3
+    private companion object {
+        const val ElementSize = 3
+    }
+}
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/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/KeyMapping.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/KeyMapping.commonStubs.kt
index e6450f8..759faf0 100644
--- a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/KeyMapping.commonStubs.kt
+++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/KeyMapping.commonStubs.kt
@@ -40,6 +40,7 @@
     actual val MoveEnd: Key = implementedInJetBrainsFork()
     actual val Insert: Key = implementedInJetBrainsFork()
     actual val Enter: Key = implementedInJetBrainsFork()
+    actual val NumPadEnter: Key = implementedInJetBrainsFork()
     actual val Backspace: Key = implementedInJetBrainsFork()
     actual val Delete: Key = implementedInJetBrainsFork()
     actual val Paste: Key = implementedInJetBrainsFork()
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/1.1.0-beta01.txt b/compose/material3/adaptive/adaptive-layout/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..b4cd5c9
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/api/1.1.0-beta01.txt
@@ -0,0 +1,469 @@
+// Signature format: 4.0
+package androidx.compose.material3.adaptive.layout {
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface AdaptStrategy {
+    method public String adapt();
+    field public static final androidx.compose.material3.adaptive.layout.AdaptStrategy.Companion Companion;
+  }
+
+  public static final class AdaptStrategy.Companion {
+    method public androidx.compose.material3.adaptive.layout.AdaptStrategy getHide();
+    property public final androidx.compose.material3.adaptive.layout.AdaptStrategy Hide;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public interface AnimatedPaneOverride {
+    method @androidx.compose.runtime.Composable public <S, T extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<S>> void AnimatedPane(androidx.compose.material3.adaptive.layout.AnimatedPaneOverrideContext<S,T>);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public final class AnimatedPaneOverrideContext<S, T extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<S>> {
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> getBoundsAnimationSpec();
+    method public kotlin.jvm.functions.Function1<androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> getContent();
+    method public androidx.compose.animation.EnterTransition getEnterTransition();
+    method public androidx.compose.animation.ExitTransition getExitTransition();
+    method public androidx.compose.ui.Modifier getModifier();
+    method public androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<S,T> getScope();
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> boundsAnimationSpec;
+    property public final kotlin.jvm.functions.Function1<androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content;
+    property public final androidx.compose.animation.EnterTransition enterTransition;
+    property public final androidx.compose.animation.ExitTransition exitTransition;
+    property public final androidx.compose.ui.Modifier modifier;
+    property public final androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<S,T> scope;
+  }
+
+  public sealed interface AnimatedPaneScope extends androidx.compose.animation.AnimatedVisibilityScope {
+    field public static final androidx.compose.material3.adaptive.layout.AnimatedPaneScope.Companion Companion;
+  }
+
+  public static final class AnimatedPaneScope.Companion {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public androidx.compose.material3.adaptive.layout.AnimatedPaneScope create(androidx.compose.animation.AnimatedVisibilityScope animatedVisibilityScope);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ExtendedPaneScaffoldPaneScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<Role,ScaffoldValue> androidx.compose.material3.adaptive.layout.PaneScaffoldPaneScope<Role> {
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ExtendedPaneScaffoldScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope androidx.compose.material3.adaptive.layout.PaneScaffoldTransitionScope<Role,ScaffoldValue> {
+  }
+
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class HingePolicy {
+    field public static final androidx.compose.material3.adaptive.layout.HingePolicy.Companion Companion;
+  }
+
+  public static final class HingePolicy.Companion {
+    method public int getAlwaysAvoid();
+    method public int getAvoidOccluding();
+    method public int getAvoidSeparating();
+    method public int getNeverAvoid();
+    property public final int AlwaysAvoid;
+    property public final int AvoidOccluding;
+    property public final int AvoidSeparating;
+    property public final int NeverAvoid;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ListDetailPaneScaffoldDefaults {
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy detailPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy listPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
+    field public static final androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldDefaults INSTANCE;
+  }
+
+  public final class ListDetailPaneScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, 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);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(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> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, 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);
+  }
+
+  public final class ListDetailPaneScaffoldRole {
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getDetail();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getExtra();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getList();
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Detail;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Extra;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole List;
+    field public static final androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole INSTANCE;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class MutableThreePaneScaffoldState extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState {
+    ctor public MutableThreePaneScaffoldState(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue initialScaffoldValue);
+    method public suspend Object? animateTo(optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? animationSpec, optional boolean isPredictiveBackInProgress, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
+    method @FloatRange(from=0.0, to=1.0) public float getProgressFraction();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
+    method public boolean isPredictiveBackInProgress();
+    method public suspend Object? seekTo(@FloatRange(from=0.0, to=1.0) float fraction, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional boolean isPredictiveBackInProgress, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? snapTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional boolean isPredictiveBackInProgress, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
+    property public boolean isPredictiveBackInProgress;
+    property @FloatRange(from=0.0, to=1.0) public float progressFraction;
+    property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
+  }
+
+  @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
+    field public static final androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion Companion;
+  }
+
+  public static final class PaneAdaptedValue.Companion {
+    method public String getExpanded();
+    method public String getHidden();
+    property public final String Expanded;
+    property public final String Hidden;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public abstract sealed class PaneExpansionAnchor {
+    method @androidx.compose.runtime.Composable public abstract String getDescription();
+    property @androidx.compose.runtime.Composable public abstract 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 {
+    ctor public PaneExpansionAnchor.Proportion(@FloatRange(from=0.0, to=1.0) float proportion);
+    method @androidx.compose.runtime.Composable public String getDescription();
+    method public float getProportion();
+    property @androidx.compose.runtime.Composable public String description;
+    property @FloatRange(from=0.0, to=1.0) public final float proportion;
+  }
+
+  public final class PaneExpansionDraggableModifierKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static kotlin.jvm.functions.Function1<androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> defaultDragHandleSemantics(androidx.compose.material3.adaptive.layout.PaneExpansionState);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class PaneExpansionState {
+    method public suspend Object? animateTo(androidx.compose.material3.adaptive.layout.PaneExpansionAnchor anchor, optional float initialVelocity, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public void clear();
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor? getCurrentAnchor();
+    method public boolean isUnspecified();
+    method public void setFirstPaneProportion(@FloatRange(from=0.0, to=1.0) float firstPaneProportion);
+    method public void setFirstPaneWidth(int firstPaneWidth);
+    property public final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor? currentAnchor;
+    field public static final androidx.compose.material3.adaptive.layout.PaneExpansionState.Companion Companion;
+    field public static final int Unspecified = -1; // 0xffffffff
+  }
+
+  public static final class PaneExpansionState.Companion {
+    property public static final int Unspecified;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public sealed interface PaneExpansionStateKey {
+    field public static final androidx.compose.material3.adaptive.layout.PaneExpansionStateKey.Companion Companion;
+  }
+
+  public static final class PaneExpansionStateKey.Companion {
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getDefault();
+    property public final androidx.compose.material3.adaptive.layout.PaneExpansionStateKey Default;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface PaneExpansionStateKeyProvider {
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getPaneExpansionStateKey();
+    property public abstract androidx.compose.material3.adaptive.layout.PaneExpansionStateKey paneExpansionStateKey;
+  }
+
+  public final class PaneExpansionStateKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.layout.PaneExpansionState rememberPaneExpansionState(optional androidx.compose.material3.adaptive.layout.PaneExpansionStateKey key, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor> anchors, optional int initialAnchoredIndex, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> anchoringAnimationSpec, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.layout.PaneExpansionState rememberPaneExpansionState(androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider keyProvider, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor> anchors, optional int initialAnchoredIndex, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> anchoringAnimationSpec, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior);
+  }
+
+  public final class PaneKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <S, T extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<S>> void AnimatedPane(androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<S,T>, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> boundsAnimationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.AnimatedPaneOverride> getLocalAnimatedPaneOverride();
+    property @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.AnimatedPaneOverride> LocalAnimatedPaneOverride;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface PaneMotion {
+    method public int getType();
+    property public abstract int type;
+    field public static final androidx.compose.material3.adaptive.layout.PaneMotion.Companion Companion;
+  }
+
+  public static final class PaneMotion.Companion {
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getAnimateBounds();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromLeft();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromLeftDelayed();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromRight();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromRightDelayed();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterWithExpand();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getExitToLeft();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getExitToRight();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getExitWithShrink();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getNoMotion();
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion AnimateBounds;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromLeft;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromLeftDelayed;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromRight;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromRightDelayed;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterWithExpand;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitToLeft;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitToRight;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitWithShrink;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion NoMotion;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public static final value class PaneMotion.Type {
+    method public int getValue();
+    property public final int value;
+    field public static final androidx.compose.material3.adaptive.layout.PaneMotion.Type.Companion Companion;
+  }
+
+  public static final class PaneMotion.Type.Companion {
+    method public int getEntering();
+    method public int getExiting();
+    method public int getHidden();
+    method public int getShown();
+    property public final int Entering;
+    property public final int Exiting;
+    property public final int Hidden;
+    property public final int Shown;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class PaneMotionData {
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getMotion();
+    method public long getOriginPosition();
+    method public long getOriginSize();
+    method public long getTargetPosition();
+    method public long getTargetSize();
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion motion;
+    property public final long originPosition;
+    property public final long originSize;
+    property public final long targetPosition;
+    property public final long targetSize;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class PaneMotionDefaults {
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> getAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> getDelayedAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getDelayedOffsetAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> getDelayedSizeAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getOffsetAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> getSizeAnimationSpec();
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> AnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> DelayedAnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> DelayedOffsetAnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> DelayedSizeAnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> OffsetAnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> SizeAnimationSpec;
+    field public static final androidx.compose.material3.adaptive.layout.PaneMotionDefaults INSTANCE;
+  }
+
+  public final class PaneMotionKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static <Role> androidx.compose.animation.EnterTransition calculateDefaultEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, Role role);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static <Role> androidx.compose.animation.ExitTransition calculateDefaultExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, Role role);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static inline <Role> void forEach(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, kotlin.jvm.functions.Function2<? super Role,? super androidx.compose.material3.adaptive.layout.PaneMotionData,kotlin.Unit> action);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static inline <Role> void forEachReversed(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, kotlin.jvm.functions.Function2<? super Role,? super androidx.compose.material3.adaptive.layout.PaneMotionData,kotlin.Unit> action);
+  }
+
+  @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
+    ctor public PaneScaffoldDirective(int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, float defaultPanePreferredWidth, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective copy(optional int maxHorizontalPartitions, optional float horizontalPartitionSpacerSize, optional int maxVerticalPartitions, optional float verticalPartitionSpacerSize, optional float defaultPanePreferredWidth, optional java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
+    method public float getDefaultPanePreferredWidth();
+    method public java.util.List<androidx.compose.ui.geometry.Rect> getExcludedBounds();
+    method public float getHorizontalPartitionSpacerSize();
+    method public int getMaxHorizontalPartitions();
+    method public int getMaxVerticalPartitions();
+    method public float getVerticalPartitionSpacerSize();
+    property public final float defaultPanePreferredWidth;
+    property public final java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds;
+    property public final float horizontalPartitionSpacerSize;
+    property public final int maxHorizontalPartitions;
+    property public final int maxVerticalPartitions;
+    property public final float verticalPartitionSpacerSize;
+    field public static final androidx.compose.material3.adaptive.layout.PaneScaffoldDirective.Companion Companion;
+  }
+
+  public static final class PaneScaffoldDirective.Companion {
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective getDefault();
+    property public final androidx.compose.material3.adaptive.layout.PaneScaffoldDirective Default;
+  }
+
+  public final class PaneScaffoldDirectiveKt {
+    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> {
+    method public operator androidx.compose.material3.adaptive.layout.PaneMotionData get(int index);
+    method public operator androidx.compose.material3.adaptive.layout.PaneMotionData get(Role role);
+    method public int getCount();
+    method public Role getRoleAt(int index);
+    method public long getScaffoldSize();
+    property public abstract int count;
+    property public abstract long scaffoldSize;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldPaneScope<Role> {
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getPaneMotion();
+    method public Role getPaneRole();
+    property public abstract androidx.compose.material3.adaptive.layout.PaneMotion paneMotion;
+    property public abstract Role paneRole;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldParentData {
+    method public float getMinTouchTargetSize();
+    method public float getPreferredWidth();
+    method public boolean isAnimatedPane();
+    property public abstract boolean isAnimatedPane;
+    property public abstract float minTouchTargetSize;
+    property public abstract float preferredWidth;
+  }
+
+  public sealed interface PaneScaffoldScope {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public androidx.compose.ui.Modifier paneExpansionDraggable(androidx.compose.ui.Modifier, androidx.compose.material3.adaptive.layout.PaneExpansionState state, float minTouchTargetSize, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> semanticsProperties);
+    method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldTransitionScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> {
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role> getMotionDataProvider();
+    method @FloatRange(from=0.0, to=1.0) public float getMotionProgress();
+    method public androidx.compose.animation.core.Transition<ScaffoldValue> getScaffoldStateTransition();
+    property public abstract androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role> motionDataProvider;
+    property @FloatRange(from=0.0, to=1.0) public abstract float motionProgress;
+    property public abstract androidx.compose.animation.core.Transition<ScaffoldValue> scaffoldStateTransition;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldValue<T> {
+    method public operator String get(T role);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldDefaults {
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy mainPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy supportingPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
+    field public static final androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldDefaults INSTANCE;
+  }
+
+  public final class SupportingPaneScaffoldKt {
+    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.ThreePaneScaffoldState scaffoldState, 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);
+    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);
+  }
+
+  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();
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Extra;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Main;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Supporting;
+    field public static final androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole INSTANCE;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneMotion {
+    method public operator androidx.compose.material3.adaptive.layout.PaneMotion get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
+    field public static final androidx.compose.material3.adaptive.layout.ThreePaneMotion.Companion Companion;
+  }
+
+  public static final class ThreePaneMotion.Companion {
+    method public androidx.compose.material3.adaptive.layout.ThreePaneMotion getNoMotion();
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneMotion NoMotion;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldAdaptStrategies {
+    ctor public ThreePaneScaffoldAdaptStrategies(androidx.compose.material3.adaptive.layout.AdaptStrategy primaryPaneAdaptStrategy, androidx.compose.material3.adaptive.layout.AdaptStrategy secondaryPaneAdaptStrategy, androidx.compose.material3.adaptive.layout.AdaptStrategy tertiaryPaneAdaptStrategy);
+    method public operator androidx.compose.material3.adaptive.layout.AdaptStrategy get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
+  }
+
+  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();
+    property public final T? contentKey;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldHorizontalOrder {
+    method public void forEach(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,kotlin.Unit> action);
+    method public void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,kotlin.Unit> action);
+    method public void forEachIndexedReversed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,kotlin.Unit> action);
+    method public operator androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole get(int index);
+    method public int getSize();
+    method public int indexOf(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
+    property public int size;
+  }
+
+  public final class ThreePaneScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride> getLocalThreePaneScaffoldOverride();
+    property @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride> LocalThreePaneScaffoldOverride;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public interface ThreePaneScaffoldOverride {
+    method @androidx.compose.runtime.Composable public void ThreePaneScaffold(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverrideContext);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public final class ThreePaneScaffoldOverrideContext {
+    method public androidx.compose.ui.Modifier getModifier();
+    method public kotlin.jvm.functions.Function1<androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? getPaneExpansionDragHandle();
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionState getPaneExpansionState();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder getPaneOrder();
+    method public kotlin.jvm.functions.Function0<kotlin.Unit> getPrimaryPane();
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective getScaffoldDirective();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState getScaffoldState();
+    method public kotlin.jvm.functions.Function0<kotlin.Unit> getSecondaryPane();
+    method public kotlin.jvm.functions.Function0<kotlin.Unit>? getTertiaryPane();
+    property public final androidx.compose.ui.Modifier modifier;
+    property public final kotlin.jvm.functions.Function1<androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle;
+    property public final androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder paneOrder;
+    property public final kotlin.jvm.functions.Function0<kotlin.Unit> primaryPane;
+    property public final androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState;
+    property public final kotlin.jvm.functions.Function0<kotlin.Unit> secondaryPane;
+    property public final kotlin.jvm.functions.Function0<kotlin.Unit>? tertiaryPane;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldPaneScope extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
+  }
+
+  public enum ThreePaneScaffoldRole {
+    enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Primary;
+    enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Secondary;
+    enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Tertiary;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public abstract sealed class ThreePaneScaffoldState {
+    method public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
+    method @FloatRange(from=0.0, to=1.0) public abstract float getProgressFraction();
+    method public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
+    method public abstract boolean isPredictiveBackInProgress();
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
+    property public abstract boolean isPredictiveBackInProgress;
+    property @FloatRange(from=0.0, to=1.0) public abstract float progressFraction;
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
+  }
+
+  @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();
+    method public String getPrimary();
+    method public String getSecondary();
+    method public String getTertiary();
+    property public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey paneExpansionStateKey;
+    property public final String primary;
+    property public final String secondary;
+    property public final String tertiary;
+  }
+
+  public final class ThreePaneScaffoldValueKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends java.lang.Object?>? currentDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends java.lang.Object?>> destinationHistory);
+  }
+
+}
+
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 cbaeb22..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 {
@@ -131,10 +147,13 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class PaneExpansionState {
+    method public suspend Object? animateTo(androidx.compose.material3.adaptive.layout.PaneExpansionAnchor anchor, optional float initialVelocity, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public void clear();
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor? getCurrentAnchor();
     method public boolean isUnspecified();
     method public void setFirstPaneProportion(@FloatRange(from=0.0, to=1.0) float firstPaneProportion);
     method public void setFirstPaneWidth(int firstPaneWidth);
+    property public final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor? currentAnchor;
     field public static final androidx.compose.material3.adaptive.layout.PaneExpansionState.Companion Companion;
     field public static final int Unspecified = -1; // 0xffffffff
   }
@@ -168,12 +187,8 @@
     property @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.AnimatedPaneOverride> LocalAnimatedPaneOverride;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneMotion {
-    method public androidx.compose.animation.EnterTransition getEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<? extends java.lang.Object?>);
-    method public androidx.compose.animation.ExitTransition getExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<? extends java.lang.Object?>);
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface PaneMotion {
     method public int getType();
-    property public androidx.compose.animation.EnterTransition enterTransition;
-    property public androidx.compose.animation.ExitTransition exitTransition;
     property public abstract int type;
     field public static final androidx.compose.material3.adaptive.layout.PaneMotion.Companion Companion;
   }
@@ -248,6 +263,8 @@
   }
 
   public final class PaneMotionKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static <Role> androidx.compose.animation.EnterTransition calculateDefaultEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, Role role);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static <Role> androidx.compose.animation.ExitTransition calculateDefaultExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, Role role);
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static inline <Role> void forEach(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, kotlin.jvm.functions.Function2<? super Role,? super androidx.compose.material3.adaptive.layout.PaneMotionData,kotlin.Unit> action);
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static inline <Role> void forEachReversed(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, kotlin.jvm.functions.Function2<? super Role,? super androidx.compose.material3.adaptive.layout.PaneMotionData,kotlin.Unit> action);
   }
@@ -276,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> {
@@ -312,13 +329,9 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldTransitionScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> {
-    method public default androidx.compose.animation.EnterTransition getEnterTransition(androidx.compose.material3.adaptive.layout.PaneMotion);
-    method public default androidx.compose.animation.ExitTransition getExitTransition(androidx.compose.material3.adaptive.layout.PaneMotion);
     method public androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role> getMotionDataProvider();
     method @FloatRange(from=0.0, to=1.0) public float getMotionProgress();
     method public androidx.compose.animation.core.Transition<ScaffoldValue> getScaffoldStateTransition();
-    property public androidx.compose.animation.EnterTransition enterTransition;
-    property public androidx.compose.animation.ExitTransition exitTransition;
     property public abstract androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role> motionDataProvider;
     property @FloatRange(from=0.0, to=1.0) public abstract float motionProgress;
     property public abstract androidx.compose.animation.core.Transition<ScaffoldValue> scaffoldStateTransition;
@@ -338,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();
@@ -363,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();
@@ -434,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/res-1.1.0-beta01.txt b/compose/material3/adaptive/adaptive-layout/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/api/res-1.1.0-beta01.txt
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_1.1.0-beta01.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..b4cd5c9
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,469 @@
+// Signature format: 4.0
+package androidx.compose.material3.adaptive.layout {
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface AdaptStrategy {
+    method public String adapt();
+    field public static final androidx.compose.material3.adaptive.layout.AdaptStrategy.Companion Companion;
+  }
+
+  public static final class AdaptStrategy.Companion {
+    method public androidx.compose.material3.adaptive.layout.AdaptStrategy getHide();
+    property public final androidx.compose.material3.adaptive.layout.AdaptStrategy Hide;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public interface AnimatedPaneOverride {
+    method @androidx.compose.runtime.Composable public <S, T extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<S>> void AnimatedPane(androidx.compose.material3.adaptive.layout.AnimatedPaneOverrideContext<S,T>);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public final class AnimatedPaneOverrideContext<S, T extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<S>> {
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> getBoundsAnimationSpec();
+    method public kotlin.jvm.functions.Function1<androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> getContent();
+    method public androidx.compose.animation.EnterTransition getEnterTransition();
+    method public androidx.compose.animation.ExitTransition getExitTransition();
+    method public androidx.compose.ui.Modifier getModifier();
+    method public androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<S,T> getScope();
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> boundsAnimationSpec;
+    property public final kotlin.jvm.functions.Function1<androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content;
+    property public final androidx.compose.animation.EnterTransition enterTransition;
+    property public final androidx.compose.animation.ExitTransition exitTransition;
+    property public final androidx.compose.ui.Modifier modifier;
+    property public final androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<S,T> scope;
+  }
+
+  public sealed interface AnimatedPaneScope extends androidx.compose.animation.AnimatedVisibilityScope {
+    field public static final androidx.compose.material3.adaptive.layout.AnimatedPaneScope.Companion Companion;
+  }
+
+  public static final class AnimatedPaneScope.Companion {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public androidx.compose.material3.adaptive.layout.AnimatedPaneScope create(androidx.compose.animation.AnimatedVisibilityScope animatedVisibilityScope);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ExtendedPaneScaffoldPaneScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<Role,ScaffoldValue> androidx.compose.material3.adaptive.layout.PaneScaffoldPaneScope<Role> {
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ExtendedPaneScaffoldScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope androidx.compose.material3.adaptive.layout.PaneScaffoldTransitionScope<Role,ScaffoldValue> {
+  }
+
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class HingePolicy {
+    field public static final androidx.compose.material3.adaptive.layout.HingePolicy.Companion Companion;
+  }
+
+  public static final class HingePolicy.Companion {
+    method public int getAlwaysAvoid();
+    method public int getAvoidOccluding();
+    method public int getAvoidSeparating();
+    method public int getNeverAvoid();
+    property public final int AlwaysAvoid;
+    property public final int AvoidOccluding;
+    property public final int AvoidSeparating;
+    property public final int NeverAvoid;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ListDetailPaneScaffoldDefaults {
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy detailPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy listPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
+    field public static final androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldDefaults INSTANCE;
+  }
+
+  public final class ListDetailPaneScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, 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);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(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> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, 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);
+  }
+
+  public final class ListDetailPaneScaffoldRole {
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getDetail();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getExtra();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getList();
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Detail;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Extra;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole List;
+    field public static final androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole INSTANCE;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class MutableThreePaneScaffoldState extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState {
+    ctor public MutableThreePaneScaffoldState(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue initialScaffoldValue);
+    method public suspend Object? animateTo(optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? animationSpec, optional boolean isPredictiveBackInProgress, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
+    method @FloatRange(from=0.0, to=1.0) public float getProgressFraction();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
+    method public boolean isPredictiveBackInProgress();
+    method public suspend Object? seekTo(@FloatRange(from=0.0, to=1.0) float fraction, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional boolean isPredictiveBackInProgress, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? snapTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional boolean isPredictiveBackInProgress, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
+    property public boolean isPredictiveBackInProgress;
+    property @FloatRange(from=0.0, to=1.0) public float progressFraction;
+    property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
+  }
+
+  @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
+    field public static final androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion Companion;
+  }
+
+  public static final class PaneAdaptedValue.Companion {
+    method public String getExpanded();
+    method public String getHidden();
+    property public final String Expanded;
+    property public final String Hidden;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public abstract sealed class PaneExpansionAnchor {
+    method @androidx.compose.runtime.Composable public abstract String getDescription();
+    property @androidx.compose.runtime.Composable public abstract 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 {
+    ctor public PaneExpansionAnchor.Proportion(@FloatRange(from=0.0, to=1.0) float proportion);
+    method @androidx.compose.runtime.Composable public String getDescription();
+    method public float getProportion();
+    property @androidx.compose.runtime.Composable public String description;
+    property @FloatRange(from=0.0, to=1.0) public final float proportion;
+  }
+
+  public final class PaneExpansionDraggableModifierKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static kotlin.jvm.functions.Function1<androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> defaultDragHandleSemantics(androidx.compose.material3.adaptive.layout.PaneExpansionState);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class PaneExpansionState {
+    method public suspend Object? animateTo(androidx.compose.material3.adaptive.layout.PaneExpansionAnchor anchor, optional float initialVelocity, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public void clear();
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor? getCurrentAnchor();
+    method public boolean isUnspecified();
+    method public void setFirstPaneProportion(@FloatRange(from=0.0, to=1.0) float firstPaneProportion);
+    method public void setFirstPaneWidth(int firstPaneWidth);
+    property public final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor? currentAnchor;
+    field public static final androidx.compose.material3.adaptive.layout.PaneExpansionState.Companion Companion;
+    field public static final int Unspecified = -1; // 0xffffffff
+  }
+
+  public static final class PaneExpansionState.Companion {
+    property public static final int Unspecified;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public sealed interface PaneExpansionStateKey {
+    field public static final androidx.compose.material3.adaptive.layout.PaneExpansionStateKey.Companion Companion;
+  }
+
+  public static final class PaneExpansionStateKey.Companion {
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getDefault();
+    property public final androidx.compose.material3.adaptive.layout.PaneExpansionStateKey Default;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface PaneExpansionStateKeyProvider {
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getPaneExpansionStateKey();
+    property public abstract androidx.compose.material3.adaptive.layout.PaneExpansionStateKey paneExpansionStateKey;
+  }
+
+  public final class PaneExpansionStateKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.layout.PaneExpansionState rememberPaneExpansionState(optional androidx.compose.material3.adaptive.layout.PaneExpansionStateKey key, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor> anchors, optional int initialAnchoredIndex, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> anchoringAnimationSpec, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.layout.PaneExpansionState rememberPaneExpansionState(androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider keyProvider, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor> anchors, optional int initialAnchoredIndex, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> anchoringAnimationSpec, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior);
+  }
+
+  public final class PaneKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <S, T extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<S>> void AnimatedPane(androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<S,T>, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> boundsAnimationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.AnimatedPaneOverride> getLocalAnimatedPaneOverride();
+    property @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.AnimatedPaneOverride> LocalAnimatedPaneOverride;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface PaneMotion {
+    method public int getType();
+    property public abstract int type;
+    field public static final androidx.compose.material3.adaptive.layout.PaneMotion.Companion Companion;
+  }
+
+  public static final class PaneMotion.Companion {
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getAnimateBounds();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromLeft();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromLeftDelayed();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromRight();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromRightDelayed();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterWithExpand();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getExitToLeft();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getExitToRight();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getExitWithShrink();
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getNoMotion();
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion AnimateBounds;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromLeft;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromLeftDelayed;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromRight;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromRightDelayed;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterWithExpand;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitToLeft;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitToRight;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitWithShrink;
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion NoMotion;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public static final value class PaneMotion.Type {
+    method public int getValue();
+    property public final int value;
+    field public static final androidx.compose.material3.adaptive.layout.PaneMotion.Type.Companion Companion;
+  }
+
+  public static final class PaneMotion.Type.Companion {
+    method public int getEntering();
+    method public int getExiting();
+    method public int getHidden();
+    method public int getShown();
+    property public final int Entering;
+    property public final int Exiting;
+    property public final int Hidden;
+    property public final int Shown;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class PaneMotionData {
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getMotion();
+    method public long getOriginPosition();
+    method public long getOriginSize();
+    method public long getTargetPosition();
+    method public long getTargetSize();
+    property public final androidx.compose.material3.adaptive.layout.PaneMotion motion;
+    property public final long originPosition;
+    property public final long originSize;
+    property public final long targetPosition;
+    property public final long targetSize;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class PaneMotionDefaults {
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> getAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> getDelayedAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getDelayedOffsetAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> getDelayedSizeAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getOffsetAnimationSpec();
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> getSizeAnimationSpec();
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> AnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntRect> DelayedAnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> DelayedOffsetAnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> DelayedSizeAnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> OffsetAnimationSpec;
+    property public final androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> SizeAnimationSpec;
+    field public static final androidx.compose.material3.adaptive.layout.PaneMotionDefaults INSTANCE;
+  }
+
+  public final class PaneMotionKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static <Role> androidx.compose.animation.EnterTransition calculateDefaultEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, Role role);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static <Role> androidx.compose.animation.ExitTransition calculateDefaultExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, Role role);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static inline <Role> void forEach(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, kotlin.jvm.functions.Function2<? super Role,? super androidx.compose.material3.adaptive.layout.PaneMotionData,kotlin.Unit> action);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static inline <Role> void forEachReversed(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, kotlin.jvm.functions.Function2<? super Role,? super androidx.compose.material3.adaptive.layout.PaneMotionData,kotlin.Unit> action);
+  }
+
+  @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
+    ctor public PaneScaffoldDirective(int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, float defaultPanePreferredWidth, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective copy(optional int maxHorizontalPartitions, optional float horizontalPartitionSpacerSize, optional int maxVerticalPartitions, optional float verticalPartitionSpacerSize, optional float defaultPanePreferredWidth, optional java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
+    method public float getDefaultPanePreferredWidth();
+    method public java.util.List<androidx.compose.ui.geometry.Rect> getExcludedBounds();
+    method public float getHorizontalPartitionSpacerSize();
+    method public int getMaxHorizontalPartitions();
+    method public int getMaxVerticalPartitions();
+    method public float getVerticalPartitionSpacerSize();
+    property public final float defaultPanePreferredWidth;
+    property public final java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds;
+    property public final float horizontalPartitionSpacerSize;
+    property public final int maxHorizontalPartitions;
+    property public final int maxVerticalPartitions;
+    property public final float verticalPartitionSpacerSize;
+    field public static final androidx.compose.material3.adaptive.layout.PaneScaffoldDirective.Companion Companion;
+  }
+
+  public static final class PaneScaffoldDirective.Companion {
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective getDefault();
+    property public final androidx.compose.material3.adaptive.layout.PaneScaffoldDirective Default;
+  }
+
+  public final class PaneScaffoldDirectiveKt {
+    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> {
+    method public operator androidx.compose.material3.adaptive.layout.PaneMotionData get(int index);
+    method public operator androidx.compose.material3.adaptive.layout.PaneMotionData get(Role role);
+    method public int getCount();
+    method public Role getRoleAt(int index);
+    method public long getScaffoldSize();
+    property public abstract int count;
+    property public abstract long scaffoldSize;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldPaneScope<Role> {
+    method public androidx.compose.material3.adaptive.layout.PaneMotion getPaneMotion();
+    method public Role getPaneRole();
+    property public abstract androidx.compose.material3.adaptive.layout.PaneMotion paneMotion;
+    property public abstract Role paneRole;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldParentData {
+    method public float getMinTouchTargetSize();
+    method public float getPreferredWidth();
+    method public boolean isAnimatedPane();
+    property public abstract boolean isAnimatedPane;
+    property public abstract float minTouchTargetSize;
+    property public abstract float preferredWidth;
+  }
+
+  public sealed interface PaneScaffoldScope {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public androidx.compose.ui.Modifier paneExpansionDraggable(androidx.compose.ui.Modifier, androidx.compose.material3.adaptive.layout.PaneExpansionState state, float minTouchTargetSize, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> semanticsProperties);
+    method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldTransitionScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> {
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role> getMotionDataProvider();
+    method @FloatRange(from=0.0, to=1.0) public float getMotionProgress();
+    method public androidx.compose.animation.core.Transition<ScaffoldValue> getScaffoldStateTransition();
+    property public abstract androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role> motionDataProvider;
+    property @FloatRange(from=0.0, to=1.0) public abstract float motionProgress;
+    property public abstract androidx.compose.animation.core.Transition<ScaffoldValue> scaffoldStateTransition;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldValue<T> {
+    method public operator String get(T role);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldDefaults {
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy mainPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy supportingPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
+    field public static final androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldDefaults INSTANCE;
+  }
+
+  public final class SupportingPaneScaffoldKt {
+    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.ThreePaneScaffoldState scaffoldState, 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);
+    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);
+  }
+
+  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();
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Extra;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Main;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Supporting;
+    field public static final androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole INSTANCE;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneMotion {
+    method public operator androidx.compose.material3.adaptive.layout.PaneMotion get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
+    field public static final androidx.compose.material3.adaptive.layout.ThreePaneMotion.Companion Companion;
+  }
+
+  public static final class ThreePaneMotion.Companion {
+    method public androidx.compose.material3.adaptive.layout.ThreePaneMotion getNoMotion();
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneMotion NoMotion;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldAdaptStrategies {
+    ctor public ThreePaneScaffoldAdaptStrategies(androidx.compose.material3.adaptive.layout.AdaptStrategy primaryPaneAdaptStrategy, androidx.compose.material3.adaptive.layout.AdaptStrategy secondaryPaneAdaptStrategy, androidx.compose.material3.adaptive.layout.AdaptStrategy tertiaryPaneAdaptStrategy);
+    method public operator androidx.compose.material3.adaptive.layout.AdaptStrategy get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
+  }
+
+  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();
+    property public final T? contentKey;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldHorizontalOrder {
+    method public void forEach(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,kotlin.Unit> action);
+    method public void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,kotlin.Unit> action);
+    method public void forEachIndexedReversed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,kotlin.Unit> action);
+    method public operator androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole get(int index);
+    method public int getSize();
+    method public int indexOf(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
+    property public int size;
+  }
+
+  public final class ThreePaneScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride> getLocalThreePaneScaffoldOverride();
+    property @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride> LocalThreePaneScaffoldOverride;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public interface ThreePaneScaffoldOverride {
+    method @androidx.compose.runtime.Composable public void ThreePaneScaffold(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverrideContext);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public final class ThreePaneScaffoldOverrideContext {
+    method public androidx.compose.ui.Modifier getModifier();
+    method public kotlin.jvm.functions.Function1<androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? getPaneExpansionDragHandle();
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionState getPaneExpansionState();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder getPaneOrder();
+    method public kotlin.jvm.functions.Function0<kotlin.Unit> getPrimaryPane();
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective getScaffoldDirective();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState getScaffoldState();
+    method public kotlin.jvm.functions.Function0<kotlin.Unit> getSecondaryPane();
+    method public kotlin.jvm.functions.Function0<kotlin.Unit>? getTertiaryPane();
+    property public final androidx.compose.ui.Modifier modifier;
+    property public final kotlin.jvm.functions.Function1<androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle;
+    property public final androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder paneOrder;
+    property public final kotlin.jvm.functions.Function0<kotlin.Unit> primaryPane;
+    property public final androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective;
+    property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState;
+    property public final kotlin.jvm.functions.Function0<kotlin.Unit> secondaryPane;
+    property public final kotlin.jvm.functions.Function0<kotlin.Unit>? tertiaryPane;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldPaneScope extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
+  }
+
+  public enum ThreePaneScaffoldRole {
+    enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Primary;
+    enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Secondary;
+    enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Tertiary;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public abstract sealed class ThreePaneScaffoldState {
+    method public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
+    method @FloatRange(from=0.0, to=1.0) public abstract float getProgressFraction();
+    method public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
+    method public abstract boolean isPredictiveBackInProgress();
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
+    property public abstract boolean isPredictiveBackInProgress;
+    property @FloatRange(from=0.0, to=1.0) public abstract float progressFraction;
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
+  }
+
+  @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();
+    method public String getPrimary();
+    method public String getSecondary();
+    method public String getTertiary();
+    property public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey paneExpansionStateKey;
+    property public final String primary;
+    property public final String secondary;
+    property public final String tertiary;
+  }
+
+  public final class ThreePaneScaffoldValueKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends java.lang.Object?>? currentDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends java.lang.Object?>> destinationHistory);
+  }
+
+}
+
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 cbaeb22..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 {
@@ -131,10 +147,13 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class PaneExpansionState {
+    method public suspend Object? animateTo(androidx.compose.material3.adaptive.layout.PaneExpansionAnchor anchor, optional float initialVelocity, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public void clear();
+    method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor? getCurrentAnchor();
     method public boolean isUnspecified();
     method public void setFirstPaneProportion(@FloatRange(from=0.0, to=1.0) float firstPaneProportion);
     method public void setFirstPaneWidth(int firstPaneWidth);
+    property public final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor? currentAnchor;
     field public static final androidx.compose.material3.adaptive.layout.PaneExpansionState.Companion Companion;
     field public static final int Unspecified = -1; // 0xffffffff
   }
@@ -168,12 +187,8 @@
     property @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.AnimatedPaneOverride> LocalAnimatedPaneOverride;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneMotion {
-    method public androidx.compose.animation.EnterTransition getEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<? extends java.lang.Object?>);
-    method public androidx.compose.animation.ExitTransition getExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<? extends java.lang.Object?>);
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface PaneMotion {
     method public int getType();
-    property public androidx.compose.animation.EnterTransition enterTransition;
-    property public androidx.compose.animation.ExitTransition exitTransition;
     property public abstract int type;
     field public static final androidx.compose.material3.adaptive.layout.PaneMotion.Companion Companion;
   }
@@ -248,6 +263,8 @@
   }
 
   public final class PaneMotionKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static <Role> androidx.compose.animation.EnterTransition calculateDefaultEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, Role role);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static <Role> androidx.compose.animation.ExitTransition calculateDefaultExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, Role role);
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static inline <Role> void forEach(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, kotlin.jvm.functions.Function2<? super Role,? super androidx.compose.material3.adaptive.layout.PaneMotionData,kotlin.Unit> action);
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static inline <Role> void forEachReversed(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role>, kotlin.jvm.functions.Function2<? super Role,? super androidx.compose.material3.adaptive.layout.PaneMotionData,kotlin.Unit> action);
   }
@@ -276,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> {
@@ -312,13 +329,9 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldTransitionScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> {
-    method public default androidx.compose.animation.EnterTransition getEnterTransition(androidx.compose.material3.adaptive.layout.PaneMotion);
-    method public default androidx.compose.animation.ExitTransition getExitTransition(androidx.compose.material3.adaptive.layout.PaneMotion);
     method public androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role> getMotionDataProvider();
     method @FloatRange(from=0.0, to=1.0) public float getMotionProgress();
     method public androidx.compose.animation.core.Transition<ScaffoldValue> getScaffoldStateTransition();
-    property public androidx.compose.animation.EnterTransition enterTransition;
-    property public androidx.compose.animation.ExitTransition exitTransition;
     property public abstract androidx.compose.material3.adaptive.layout.PaneScaffoldMotionDataProvider<Role> motionDataProvider;
     property @FloatRange(from=0.0, to=1.0) public abstract float motionProgress;
     property public abstract androidx.compose.animation.core.Transition<ScaffoldValue> scaffoldStateTransition;
@@ -338,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();
@@ -363,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();
@@ -434,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 591c322..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
@@ -34,6 +34,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.junit.Rule
@@ -178,7 +179,7 @@
             mockPaneExpansionState = rememberPaneExpansionState(anchors = MockPaneExpansionAnchors)
             mockDraggingPx = with(LocalDensity.current) { 200.dp.toPx() }
             expectedSettledOffsetPx =
-                with(LocalDensity.current) { MockPaneExpansionMiddleAnchor.toPx().toInt() }
+                with(LocalDensity.current) { MockPaneExpansionMiddleAnchor.roundToPx() }
             SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) { MockDragHandle(it) }
         }
 
@@ -302,12 +303,12 @@
                     anchors =
                         listOf(
                             PaneExpansionAnchor.Proportion(0f),
-                            PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor)
+                            PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor)
                         )
                 )
             mockDraggingPx = with(LocalDensity.current) { 200.dp.toPx() }
             expectedSettledOffsetPx =
-                with(LocalDensity.current) { MockPaneExpansionMiddleAnchor.toPx().toInt() }
+                with(LocalDensity.current) { MockPaneExpansionMiddleAnchor.roundToPx() }
             SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) { MockDragHandle(it) }
         }
 
@@ -321,6 +322,83 @@
                 .isEqualTo(expectedSettledOffsetPx)
         }
     }
+
+    @Test
+    fun threePaneScaffold_paneExpansionWithDragHandle_animateToAnchor() {
+        var expectedSettledOffsetPx = 0
+        lateinit var mockPaneExpansionState: PaneExpansionState
+        lateinit var scope: CoroutineScope
+
+        rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+            scope = rememberCoroutineScope()
+            mockPaneExpansionState = rememberPaneExpansionState(anchors = MockPaneExpansionAnchors)
+            expectedSettledOffsetPx =
+                with(LocalDensity.current) { MockPaneExpansionMiddleAnchor.roundToPx() }
+            SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) { MockDragHandle(it) }
+        }
+
+        rule.runOnIdle {
+            scope.launch {
+                mockPaneExpansionState.animateTo(
+                    PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(mockPaneExpansionState.currentMeasuredDraggingOffset)
+                .isEqualTo(expectedSettledOffsetPx)
+        }
+    }
+
+    @Test
+    fun threePaneScaffold_paneExpansionWithDragHandle_animateToAnchorWithVelocity() {
+        var expectedSettledOffsetPx = 0
+        lateinit var mockPaneExpansionState: PaneExpansionState
+        lateinit var scope: CoroutineScope
+
+        rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+            scope = rememberCoroutineScope()
+            mockPaneExpansionState = rememberPaneExpansionState(anchors = MockPaneExpansionAnchors)
+            expectedSettledOffsetPx =
+                with(LocalDensity.current) { MockPaneExpansionMiddleAnchor.roundToPx() }
+            SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) { MockDragHandle(it) }
+        }
+
+        rule.runOnIdle {
+            scope.launch {
+                mockPaneExpansionState.animateTo(
+                    PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor),
+                    200F
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(mockPaneExpansionState.currentMeasuredDraggingOffset)
+                .isEqualTo(expectedSettledOffsetPx)
+        }
+    }
+
+    @Test
+    fun threePaneScaffold_paneExpansionWithDragHandle_animateToNonExistAnchorThrows() {
+        lateinit var mockPaneExpansionState: PaneExpansionState
+        lateinit var scope: CoroutineScope
+
+        rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+            scope = rememberCoroutineScope()
+            mockPaneExpansionState = rememberPaneExpansionState(anchors = MockPaneExpansionAnchors)
+            SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) { MockDragHandle(it) }
+        }
+
+        rule.runOnIdle {
+            scope.launch {
+                assertFailsWith<IllegalArgumentException> {
+                    mockPaneExpansionState.animateTo(PaneExpansionAnchor.Offset.fromStart(10.dp))
+                }
+            }
+        }
+    }
 }
 
 private val MockScaffoldDirective = PaneScaffoldDirective.Default
@@ -333,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-af/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-af/strings.xml
new file mode 100644
index 0000000..8908ff9
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-af/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Sleephandvatsel van paneeluitbreiding"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Verander paneelverdeling na %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d persent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-am/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-am/strings.xml
new file mode 100644
index 0000000..f3ec9b8
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-am/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ar/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ar/strings.xml
new file mode 100644
index 0000000..fa48e19
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ar/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
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..b96fec7 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-az/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-az/strings.xml
new file mode 100644
index 0000000..4f33001
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-az/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Panelin genişləndirilməsi üçün sürükləmə tutacağı"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Panel bölgüsünü %s olaraq dəyişin"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d faiz"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-b+sr+Latn/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..b7fb312
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Marker za prevlačenje kojim se proširuje okno"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Promenite podeljeno okno na: %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"Procenat: %d"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-be/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-be/strings.xml
new file mode 100644
index 0000000..9287ae5
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-be/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bg/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bg/strings.xml
new file mode 100644
index 0000000..9ea24d5
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bg/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bn/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bn/strings.xml
new file mode 100644
index 0000000..88277ab
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bn/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bs/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bs/strings.xml
new file mode 100644
index 0000000..1e37f66
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-bs/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Ručica za prevlačenje radi proširenja okna"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Promjena podijeljenog okna na %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d posto"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ca/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ca/strings.xml
new file mode 100644
index 0000000..c01e276a
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ca/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Ansa per arrossegar l\'expansió de la subfinestra"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Canvia la divisió de la subfinestra a %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"percentatge de %d"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-cs/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-cs/strings.xml
new file mode 100644
index 0000000..7b7b1f5
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-cs/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Úchyt pro přetažení a rozbalení panelu"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Změnit rozdělení panelu na %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d procent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-da/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-da/strings.xml
new file mode 100644
index 0000000..b063bfa
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-da/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Håndtag til udvidelse af rude"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Skift rudeopdeling til %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d procent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-de/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-de/strings.xml
new file mode 100644
index 0000000..1288d4d
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-de/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Ziehpunkt zum Maximieren des Bereichs"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"„Fenster teilen“ in %s ändern"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d Prozent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-el/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-el/strings.xml
new file mode 100644
index 0000000..205f65d
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-el/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rAU/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..4ce37ce
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rAU/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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 per cent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</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..6712b1e 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rGB/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..4ce37ce
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rGB/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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 per cent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rIN/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..4ce37ce
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rIN/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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 per cent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-es-rUS/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..0b2032d
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-es-rUS/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Controlador de arrastre para expandir el panel"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Cambiar la división del panel a %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d por ciento"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-es/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-es/strings.xml
new file mode 100644
index 0000000..1156a91
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-es/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Controlador de arrastre para expandir el panel"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Cambiar división de panel a %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d por ciento"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-et/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-et/strings.xml
new file mode 100644
index 0000000..b4867e7
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-et/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Paani laiendamise lohistamispide"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Muutke jaotatud paan väärtusele %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d protsenti"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-eu/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-eu/strings.xml
new file mode 100644
index 0000000..ab819fa
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-eu/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Panelaren zabalera arrastatzeko kontrol-puntua"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Aldatu panelaren zatiketa eta ezarri %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"Ehuneko %d"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fa/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fa/strings.xml
new file mode 100644
index 0000000..b5becf3
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fa/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fi/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fi/strings.xml
new file mode 100644
index 0000000..76852627
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fi/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Ruudun laajennusvetokahva"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Muuta ruudun jaoksi %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d prosenttia"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fr-rCA/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..bfcfa4f
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fr-rCA/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Poignée de déplacement d\'extension du volet"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Modifiez la division du volet à %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d pour cent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fr/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fr/strings.xml
new file mode 100644
index 0000000..958e4dc
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-fr/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Poignée de déplacement pour développer les volets"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Passer la répartition des volets sur %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d pour cent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-gl/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-gl/strings.xml
new file mode 100644
index 0000000..98d43ed
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-gl/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Controlador de arrastre para despregar o panel"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Cambia o panel dividido a %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d por cento"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-gu/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-gu/strings.xml
new file mode 100644
index 0000000..4d0ea30
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-gu/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</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..106bb8f 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hr/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hr/strings.xml
new file mode 100644
index 0000000..011ce1e
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hr/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Marker za povlačenje proširenja okna"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Promijeni podjelu okna u: %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d posto"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hu/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hu/strings.xml
new file mode 100644
index 0000000..3c5b474
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hu/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Panel kibontásának fogópontja"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Panelfelosztás módosítása a következőre: %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d százalék"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hy/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hy/strings.xml
new file mode 100644
index 0000000..be56efd
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hy/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-in/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-in/strings.xml
new file mode 100644
index 0000000..e2015af
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-in/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Handel geser perluasan panel"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Ubah luas panel ganda menjadi %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d persen"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-is/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-is/strings.xml
new file mode 100644
index 0000000..0b5b73b
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-is/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Dragkló gluggastækkunar"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Breyta gluggaskiptingu í %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d prósent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-it/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-it/strings.xml
new file mode 100644
index 0000000..fd0934c
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-it/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Punto di trascinamento per l\'espansione del riquadro"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Modifica la divisione del riquadro in %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d percentuale"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-iw/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-iw/strings.xml
new file mode 100644
index 0000000..22f606e
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-iw/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</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..e82e606 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </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..1153733 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-kk/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-kk/strings.xml
new file mode 100644
index 0000000..807f362
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-kk/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-km/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-km/strings.xml
new file mode 100644
index 0000000..096f5d0
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-km/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-kn/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-kn/strings.xml
new file mode 100644
index 0000000..585e425
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-kn/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ko/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ko/strings.xml
new file mode 100644
index 0000000..6f78f96
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ko/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ky/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ky/strings.xml
new file mode 100644
index 0000000..dce8ec7
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ky/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lo/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lo/strings.xml
new file mode 100644
index 0000000..c2e618f
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lo/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lt/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lt/strings.xml
new file mode 100644
index 0000000..333a455
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lt/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Srities išplėtimo vilkimo rankenėlė"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Keisti srities skaidymą į %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d proc."</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lv/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lv/strings.xml
new file mode 100644
index 0000000..4e1a0fb
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-lv/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Rūts izvēršanas vilkšanas turis"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Mainīt rūts sadalījumu uz šādu: %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d procents(-i)"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mk/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mk/strings.xml
new file mode 100644
index 0000000..51afd11
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mk/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</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..df0ae0a 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mn/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mn/strings.xml
new file mode 100644
index 0000000..3ccb872
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mn/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mr/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mr/strings.xml
new file mode 100644
index 0000000..7579a02
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-mr/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</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..ad90a10 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-my/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-my/strings.xml
new file mode 100644
index 0000000..0b33798
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-my/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-nb/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-nb/strings.xml
new file mode 100644
index 0000000..c2b4ce3
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-nb/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Håndtak for utvidelse av feltet"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Endre feltdelingen til %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d prosent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ne/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ne/strings.xml
new file mode 100644
index 0000000..b0560f5
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ne/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-nl/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-nl/strings.xml
new file mode 100644
index 0000000..82a9d5c
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-nl/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Handgreep voor slepen om deelvenster uit te breiden"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Deelvenstersplitsing wijzigen in %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d procent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-or/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-or/strings.xml
new file mode 100644
index 0000000..f84b2f5
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-or/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pa/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pa/strings.xml
new file mode 100644
index 0000000..d139af9
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pa/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</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..d9f3cd1 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rBR/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..740a3f9
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rBR/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Alça de arrastar para expandir o painel"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Mudar a divisão do painel para %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d por cento"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</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..96cd773 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,8 @@
     <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
 </resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt/strings.xml
new file mode 100644
index 0000000..740a3f9
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Alça de arrastar para expandir o painel"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Mudar a divisão do painel para %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d por cento"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ro/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ro/strings.xml
new file mode 100644
index 0000000..8c1397e
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ro/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Ghidaj de tragere pentru extinderea panoului"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Modifică împărțirea panoului la %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d procent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ru/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ru/strings.xml
new file mode 100644
index 0000000..d04f3ca
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ru/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-si/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-si/strings.xml
new file mode 100644
index 0000000..1c5a14a1
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-si/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sk/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sk/strings.xml
new file mode 100644
index 0000000..1bcb79a
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sk/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Presúvadlo na rozšírenie panela"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Zmeniť rozdelenie panela na %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"Počet percent: %d"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sl/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sl/strings.xml
new file mode 100644
index 0000000..c7ca0b6
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sl/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Ročica za vlečenje za razširitev podokna"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Sprememba razdelitve podokna na %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d odstotkov"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sq/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sq/strings.xml
new file mode 100644
index 0000000..27b0a73
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sq/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Doreza e zvarritjes për zgjerimin e panelit"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Ndrysho ndarjen e panelit në %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d për qind"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sr/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sr/strings.xml
new file mode 100644
index 0000000..8c51385
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sr/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sv/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sv/strings.xml
new file mode 100644
index 0000000..010c964
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sv/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Handtag för rutexpansion"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Ändra rutdelning till %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d procent"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sw/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sw/strings.xml
new file mode 100644
index 0000000..06d401c
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-sw/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Aikoni ya buruta ili kupanua kijisehemu"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Badilisha hali ya kugawanya kijisehemu iwe %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"Asilimia %d"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ta/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ta/strings.xml
new file mode 100644
index 0000000..dfc1028
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ta/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-te/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-te/strings.xml
new file mode 100644
index 0000000..63c702b
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-te/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-th/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-th/strings.xml
new file mode 100644
index 0000000..94ce1e8
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-th/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-tl/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-tl/strings.xml
new file mode 100644
index 0000000..b459c2a
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-tl/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Handle sa pag-drag sa pagpapalawak ng pane"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Gawing %s ang pane split"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d (na) porsyento"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-tr/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-tr/strings.xml
new file mode 100644
index 0000000..d478356
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-tr/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Bölmeyi genişletmek için sürükleme tutamacı"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Bölme oranını %s olarak değiştirin"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"Yüzde %d"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-uk/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-uk/strings.xml
new file mode 100644
index 0000000..27680e2b
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-uk/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ur/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ur/strings.xml
new file mode 100644
index 0000000..3acbaeeb
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ur/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-uz/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-uz/strings.xml
new file mode 100644
index 0000000..36d8660
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-uz/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Panelni kengaytirish uchun surish dastagi"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Ajratilgan panelni %s sifatida oʻzgartirish"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d foiz"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-vi/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-vi/strings.xml
new file mode 100644
index 0000000..e5cf8f1
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-vi/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Nút kéo mở rộng ngăn"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Thay đổi chế độ tách ngăn thành %s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d phần trăm"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rCN/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..671e560
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rCN/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rHK/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..6309aa1
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rHK/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rTW/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..4da1690
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zh-rTW/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <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>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zu/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zu/strings.xml
new file mode 100644
index 0000000..c549752
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-zu/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Isibambo sokuhudula isandiso sepane"</string>
+    <string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Shintsha ukuhlukana kwepane kube ku-%s"</string>
+    <string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d iphesenti"</string>
+    <!-- no translation found for m3_adaptive_default_pane_expansion_start_offset_anchor_description (5056616348537665604) -->
+    <skip />
+    <!-- no translation found for m3_adaptive_default_pane_expansion_end_offset_anchor_description (6412636251656811002) -->
+    <skip />
+</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/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
index a32b7ec..0355530 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
@@ -83,12 +83,21 @@
         expectedEnterTransition: EnterTransition,
         expectedExitTransition: ExitTransition
     ) {
+        mockPaneScaffoldMotionDataProvider.updateMotions(this, NoMotion, NoMotion)
         // Can't compare equality directly because of lambda. Check string representation instead
         assertWithMessage("Enter transition of $this: ")
-            .that(mockPaneScaffoldMotionDataProvider.enterTransition.toString())
+            .that(
+                mockPaneScaffoldMotionDataProvider
+                    .calculateDefaultEnterTransition(ThreePaneScaffoldRole.Primary)
+                    .toString()
+            )
             .isEqualTo(expectedEnterTransition.toString())
         assertWithMessage("Exit transition of $this: ")
-            .that(mockPaneScaffoldMotionDataProvider.exitTransition.toString())
+            .that(
+                mockPaneScaffoldMotionDataProvider
+                    .calculateDefaultExitTransition(ThreePaneScaffoldRole.Primary)
+                    .toString()
+            )
             .isEqualTo(expectedExitTransition.toString())
     }
 
@@ -274,6 +283,32 @@
                     mockPaneScaffoldMotionDataProvider[1].currentLeft
             )
     }
+
+    @Test
+    fun hiddenPaneCurrentLeft_useRightEdgeOfLeftShownPane() {
+        mockPaneScaffoldMotionDataProvider.updateMotions(
+            ExitToLeft,
+            EnterFromRight,
+            EnterWithExpand
+        )
+        assertThat(
+                mockPaneScaffoldMotionDataProvider.getHiddenPaneCurrentLeft(
+                    ThreePaneScaffoldRole.Tertiary
+                )
+            )
+            .isEqualTo(mockPaneScaffoldMotionDataProvider[0].currentRight)
+    }
+
+    @Test
+    fun hidingPaneTargetLeft_useRightEdgeOfLeftShowingPane() {
+        mockPaneScaffoldMotionDataProvider.updateMotions(EnterFromLeft, ExitToRight, ExitWithShrink)
+        assertThat(
+                mockPaneScaffoldMotionDataProvider.getHidingPaneTargetLeft(
+                    ThreePaneScaffoldRole.Tertiary
+                )
+            )
+            .isEqualTo(mockPaneScaffoldMotionDataProvider[0].targetRight)
+    }
 }
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
@@ -471,8 +506,10 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val mockEnterWithExpandTransition =
-    expandHorizontally(PaneMotionDefaults.SizeAnimationSpec, Alignment.CenterHorizontally)
+    expandHorizontally(PaneMotionDefaults.SizeAnimationSpec, Alignment.CenterHorizontally) +
+        slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec)
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val mockExitWithShrinkTransition =
-    shrinkHorizontally(PaneMotionDefaults.SizeAnimationSpec, Alignment.CenterHorizontally)
+    shrinkHorizontally(PaneMotionDefaults.SizeAnimationSpec, Alignment.CenterHorizontally) +
+        slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec)
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
index 862c541..49dd66a 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
@@ -93,8 +93,8 @@
 @Composable
 fun <S, T : PaneScaffoldValue<S>> ExtendedPaneScaffoldPaneScope<S, T>.AnimatedPane(
     modifier: Modifier = Modifier,
-    enterTransition: EnterTransition = paneMotion.enterTransition,
-    exitTransition: ExitTransition = paneMotion.exitTransition,
+    enterTransition: EnterTransition = motionDataProvider.calculateDefaultEnterTransition(paneRole),
+    exitTransition: ExitTransition = motionDataProvider.calculateDefaultExitTransition(paneRole),
     boundsAnimationSpec: FiniteAnimationSpec<IntRect> = PaneMotionDefaults.AnimationSpec,
     content: (@Composable AnimatedPaneScope.() -> Unit),
 ) {
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 910b42e7..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
@@ -222,8 +222,14 @@
             currentMeasuredDraggingOffset = coercedValue
         }
 
-    @VisibleForTesting
-    internal var currentAnchor
+    /**
+     * The current anchor that pane expansion has been settled or is settling to. Note that this
+     * field might be `null` if:
+     * 1. No anchors have been set to the state.
+     * 2. Pane expansion is set directly via [setFirstPaneWidth] or set [setFirstPaneProportion].
+     * 3. Pane expansion is in its initial state without an initial anchor provided.
+     */
+    var currentAnchor
         get() = data.currentAnchorState
         private set(value) {
             data.currentAnchorState = value
@@ -335,6 +341,7 @@
         data.firstPaneProportionState = Float.NaN
         data.currentDraggingOffsetState = Unspecified
         data.firstPaneWidthState = firstPaneWidth
+        currentAnchor = null
     }
 
     /**
@@ -351,6 +358,24 @@
         data.firstPaneWidthState = Unspecified
         data.currentDraggingOffsetState = Unspecified
         data.firstPaneProportionState = firstPaneProportion
+        currentAnchor = null
+    }
+
+    /**
+     * Animate the pane expansion to the given [PaneExpansionAnchor]. Note that the given anchor
+     * must be one of the provided anchor when creating the state with [rememberPaneExpansionState];
+     * otherwise the function throws.
+     *
+     * @param anchor the anchor to animate to
+     * @param initialVelocity the initial velocity of the animation
+     */
+    suspend fun animateTo(anchor: PaneExpansionAnchor, initialVelocity: Float = 0F) {
+        require(anchors.contains(anchor)) { "The provided $anchor is not in the anchor list!" }
+        currentAnchor = anchor
+        measuredDensity?.apply {
+            val position = anchor.positionIn(maxExpansionWidth, this)
+            animateToInternal(position, initialVelocity)
+        }
     }
 
     /**
@@ -377,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
@@ -426,30 +451,39 @@
         }
 
         dragMutex.mutate(MutatePriority.PreventUserInput) {
-            isSettling = true
-            val leftVelocity = flingBehavior.run { dragScope.performFling(velocity) }
-            val anchorPosition =
-                measuredAnchorPositions.getPositionOfTheClosestAnchor(
-                    currentMeasuredDraggingOffset,
-                    leftVelocity
-                )
             try {
+                isSettling = true
+                val leftVelocity = flingBehavior.run { dragScope.performFling(velocity) }
+                val anchorPosition =
+                    measuredAnchorPositions.getPositionOfTheClosestAnchor(
+                        currentMeasuredDraggingOffset,
+                        leftVelocity
+                    )
                 currentAnchor = anchors[anchorPosition.index]
-                animate(
-                    currentMeasuredDraggingOffset.toFloat(),
-                    anchorPosition.position.toFloat(),
-                    leftVelocity,
-                    anchoringAnimationSpec,
-                ) { value, _ ->
-                    currentDraggingOffset = value.toInt()
-                }
+                animateToInternal(anchorPosition.position, leftVelocity)
             } finally {
-                currentDraggingOffset = anchorPosition.position
                 isSettling = false
             }
         }
     }
 
+    private suspend fun animateToInternal(offset: Int, initialVelocity: Float) {
+        try {
+            isSettling = true
+            animate(
+                currentMeasuredDraggingOffset.toFloat(),
+                offset.toFloat(),
+                initialVelocity,
+                anchoringAnimationSpec,
+            ) { value, _ ->
+                currentDraggingOffset = value.toInt()
+            }
+        } finally {
+            currentDraggingOffset = offset
+            isSettling = false
+        }
+    }
+
     private fun IndexedAnchorPositionList.getPositionOfTheClosestAnchor(
         currentPosition: Int,
         velocity: Float
@@ -554,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
@@ -582,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.toPx() }.toInt().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
     }
 }
 
@@ -691,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/PaneMotion.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
index d1c2452..984a3f89 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
@@ -31,6 +31,8 @@
 import androidx.compose.animation.slideInHorizontally
 import androidx.compose.animation.slideOutHorizontally
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntRect
@@ -350,15 +352,101 @@
         return 0
     }
 
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@VisibleForTesting
+internal fun <Role> PaneScaffoldMotionDataProvider<Role>.getHiddenPaneCurrentLeft(role: Role): Int {
+    var currentLeft = 0
+    forEach { paneRole, data ->
+        // Find the right edge of the shown pane next to the left.
+        if (paneRole == role) {
+            return currentLeft
+        }
+        if (
+            data.motion.type == PaneMotion.Type.Shown || data.motion.type == PaneMotion.Type.Exiting
+        ) {
+            currentLeft = data.currentRight
+        }
+    }
+    return currentLeft
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@VisibleForTesting
+internal fun <Role> PaneScaffoldMotionDataProvider<Role>.getHidingPaneTargetLeft(role: Role): Int {
+    var targetLeft = 0
+    forEach { paneRole, data ->
+        // Find the right edge of the shown pane next to the left.
+        if (paneRole == role) {
+            return targetLeft
+        }
+        if (
+            data.motion.type == PaneMotion.Type.Shown ||
+                data.motion.type == PaneMotion.Type.Entering
+        ) {
+            targetLeft = data.targetRight
+        }
+    }
+    return targetLeft
+}
+
+/**
+ * Calculates the default [EnterTransition] of the pane associated to the given role when it's
+ * showing. The [PaneMotion] and pane measurement data provided by [PaneScaffoldMotionDataProvider]
+ * will be used to decide the transition type and relevant values like sliding offsets.
+ *
+ * @param role the role of the pane that is supposed to perform the [EnterTransition] when showing.
+ */
+@ExperimentalMaterial3AdaptiveApi
+fun <Role> PaneScaffoldMotionDataProvider<Role>.calculateDefaultEnterTransition(role: Role) =
+    when (this[role].motion) {
+        PaneMotion.EnterFromLeft ->
+            slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec) { slideInFromLeftOffset }
+        PaneMotion.EnterFromLeftDelayed ->
+            slideInHorizontally(PaneMotionDefaults.DelayedOffsetAnimationSpec) {
+                slideInFromLeftOffset
+            }
+        PaneMotion.EnterFromRight ->
+            slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec) { slideInFromRightOffset }
+        PaneMotion.EnterFromRightDelayed ->
+            slideInHorizontally(PaneMotionDefaults.DelayedOffsetAnimationSpec) {
+                slideInFromRightOffset
+            }
+        PaneMotion.EnterWithExpand -> {
+            expandHorizontally(PaneMotionDefaults.SizeAnimationSpec, Alignment.CenterHorizontally) +
+                slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec) {
+                    getHiddenPaneCurrentLeft(role) - this[role].targetLeft
+                }
+        }
+        else -> EnterTransition.None
+    }
+
+/**
+ * Calculates the default [ExitTransition] of the pane associated to the given role when it's
+ * hiding. The [PaneMotion] and pane measurement data provided by [PaneScaffoldMotionDataProvider]
+ * will be used to decide the transition type and relevant values like sliding offsets.
+ *
+ * @param role the role of the pane that is supposed to perform the [ExitTransition] when hiding.
+ */
+@ExperimentalMaterial3AdaptiveApi
+fun <Role> PaneScaffoldMotionDataProvider<Role>.calculateDefaultExitTransition(role: Role) =
+    when (this[role].motion) {
+        PaneMotion.ExitToLeft ->
+            slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec) { slideOutToLeftOffset }
+        PaneMotion.ExitToRight ->
+            slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec) { slideOutToRightOffset }
+        PaneMotion.ExitWithShrink -> {
+            shrinkHorizontally(PaneMotionDefaults.SizeAnimationSpec, Alignment.CenterHorizontally) +
+                slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec) {
+                    getHidingPaneTargetLeft(role) - this[role].currentLeft
+                }
+        }
+        else -> ExitTransition.None
+    }
+
 /** Interface to specify a custom pane enter/exit motion when a pane's visibility changes. */
 @ExperimentalMaterial3AdaptiveApi
-interface PaneMotion {
-    /** The [EnterTransition] of a pane under the given [PaneScaffoldMotionDataProvider]. */
-    val PaneScaffoldMotionDataProvider<*>.enterTransition: EnterTransition
-
-    /** The [ExitTransition] of a pane under the given [PaneScaffoldMotionDataProvider]. */
-    val PaneScaffoldMotionDataProvider<*>.exitTransition: ExitTransition
-
+@Stable
+sealed interface PaneMotion {
     /** The type of the motion, like exiting, entering, etc. See [Type]. */
     val type: Type
 
@@ -405,138 +493,73 @@
         }
     }
 
-    private abstract class DefaultImpl(val name: String, override val type: Type) : PaneMotion {
-        override val PaneScaffoldMotionDataProvider<*>.enterTransition
-            get() = EnterTransition.None
-
-        override val PaneScaffoldMotionDataProvider<*>.exitTransition
-            get() = ExitTransition.None
-
+    @Immutable
+    private class DefaultImpl(val name: String, override val type: Type) : PaneMotion {
         override fun toString() = name
     }
 
     companion object {
         /** The default pane motion that no animation will be performed. */
-        val NoMotion: PaneMotion = object : DefaultImpl("NoMotion", Type.Hidden) {}
+        val NoMotion: PaneMotion = DefaultImpl("NoMotion", Type.Hidden)
 
         /**
          * The default pane motion that will animate panes bounds with the given animation specs
          * during motion. Note that this should only be used when the associated pane is keeping
          * showing during the motion.
          */
-        val AnimateBounds: PaneMotion = object : DefaultImpl("AnimateBounds", Type.Shown) {}
+        val AnimateBounds: PaneMotion = DefaultImpl("AnimateBounds", Type.Shown)
 
         /**
          * The default pane motion that will slide panes in from left. Note that this should only be
          * used when the associated pane is entering - i.e. becoming visible from a hidden state.
          */
-        val EnterFromLeft: PaneMotion =
-            object : DefaultImpl("EnterFromLeft", Type.Entering) {
-                override val PaneScaffoldMotionDataProvider<*>.enterTransition
-                    get() =
-                        slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec) {
-                            slideInFromLeftOffset
-                        }
-            }
+        val EnterFromLeft: PaneMotion = DefaultImpl("EnterFromLeft", Type.Entering)
 
         /**
          * The default pane motion that will slide panes in from right. Note that this should only
          * be used when the associated pane is entering - i.e. becoming visible from a hidden state.
          */
-        val EnterFromRight: PaneMotion =
-            object : DefaultImpl("EnterFromRight", Type.Entering) {
-                override val PaneScaffoldMotionDataProvider<*>.enterTransition
-                    get() =
-                        slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec) {
-                            slideInFromRightOffset
-                        }
-            }
+        val EnterFromRight: PaneMotion = DefaultImpl("EnterFromRight", Type.Entering)
 
         /**
          * The default pane motion that will slide panes in from left with a delay, usually to avoid
          * the interference of other exiting panes. Note that this should only be used when the
          * associated pane is entering - i.e. becoming visible from a hidden state.
          */
-        val EnterFromLeftDelayed: PaneMotion =
-            object : DefaultImpl("EnterFromLeftDelayed", Type.Entering) {
-                override val PaneScaffoldMotionDataProvider<*>.enterTransition
-                    get() =
-                        slideInHorizontally(PaneMotionDefaults.DelayedOffsetAnimationSpec) {
-                            slideInFromLeftOffset
-                        }
-            }
+        val EnterFromLeftDelayed: PaneMotion = DefaultImpl("EnterFromLeftDelayed", Type.Entering)
 
         /**
          * The default pane motion that will slide panes in from right with a delay, usually to
          * avoid the interference of other exiting panes. Note that this should only be used when
          * the associated pane is entering - i.e. becoming visible from a hidden state.
          */
-        val EnterFromRightDelayed: PaneMotion =
-            object : DefaultImpl("EnterFromRightDelayed", Type.Entering) {
-                override val PaneScaffoldMotionDataProvider<*>.enterTransition
-                    get() =
-                        slideInHorizontally(PaneMotionDefaults.DelayedOffsetAnimationSpec) {
-                            slideInFromRightOffset
-                        }
-            }
+        val EnterFromRightDelayed: PaneMotion = DefaultImpl("EnterFromRightDelayed", Type.Entering)
 
         /**
          * The default pane motion that will slide panes out to left. Note that this should only be
          * used when the associated pane is exiting - i.e. becoming hidden from a visible state.
          */
-        val ExitToLeft: PaneMotion =
-            object : DefaultImpl("ExitToLeft", Type.Exiting) {
-                override val PaneScaffoldMotionDataProvider<*>.exitTransition
-                    get() =
-                        slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec) {
-                            slideOutToLeftOffset
-                        }
-            }
+        val ExitToLeft: PaneMotion = DefaultImpl("ExitToLeft", Type.Exiting)
 
         /**
          * The default pane motion that will slide panes out to right. Note that this should only be
          * used when the associated pane is exiting - i.e. becoming hidden from a visible state.
          */
-        val ExitToRight: PaneMotion =
-            object : DefaultImpl("ExitToRight", Type.Exiting) {
-                override val PaneScaffoldMotionDataProvider<*>.exitTransition
-                    get() =
-                        slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec) {
-                            slideOutToRightOffset
-                        }
-            }
+        val ExitToRight: PaneMotion = DefaultImpl("ExitToRight", Type.Exiting)
 
         /**
          * The default pane motion that will expand panes from a zero size. Note that this should
          * only be used when the associated pane is entering - i.e. becoming visible from a hidden
          * state.
          */
-        val EnterWithExpand: PaneMotion =
-            object : DefaultImpl("EnterWithExpand", Type.Entering) {
-                // TODO(conradchen): Expand with position change
-                override val PaneScaffoldMotionDataProvider<*>.enterTransition
-                    get() =
-                        expandHorizontally(
-                            PaneMotionDefaults.SizeAnimationSpec,
-                            Alignment.CenterHorizontally
-                        )
-            }
+        val EnterWithExpand: PaneMotion = DefaultImpl("EnterWithExpand", Type.Entering)
 
         /**
          * The default pane motion that will shrink panes until it's gone. Note that this should
          * only be used when the associated pane is exiting - i.e. becoming hidden from a visible
          * state.
          */
-        val ExitWithShrink: PaneMotion =
-            object : DefaultImpl("ExitWithShrink", Type.Exiting) {
-                // TODO(conradchen): Shrink with position change
-                override val PaneScaffoldMotionDataProvider<*>.exitTransition
-                    get() =
-                        shrinkHorizontally(
-                            PaneMotionDefaults.SizeAnimationSpec,
-                            Alignment.CenterHorizontally
-                        )
-            }
+        val ExitWithShrink: PaneMotion = DefaultImpl("ExitWithShrink", Type.Exiting)
     }
 }
 
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index df9e946..0607f9f 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -17,7 +17,6 @@
 package androidx.compose.material3.adaptive.layout
 
 import androidx.annotation.FloatRange
-import androidx.compose.animation.EnterTransition
 import androidx.compose.animation.core.Transition
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
@@ -32,7 +31,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastMaxOfOrNull
+import androidx.compose.ui.unit.isSpecified
 
 /**
  * Extended scope for the panes of pane scaffolds. All pane scaffolds will implement this interface
@@ -125,24 +124,6 @@
      * behavior.
      */
     val motionDataProvider: PaneScaffoldMotionDataProvider<Role>
-
-    /**
-     * A convenient function to get the given [PaneMotion]'s [EnterTransition] under the context of
-     * the current [PaneScaffoldTransitionScope].
-     *
-     * @see [PaneMotion.enterTransition]
-     */
-    val PaneMotion.enterTransition
-        get() = with(this) { motionDataProvider.enterTransition }
-
-    /**
-     * A convenient function to get the given [PaneMotion]'s [EnterTransition] under the context of
-     * the current [PaneScaffoldTransitionScope].
-     *
-     * @see [PaneMotion.exitTransition]
-     */
-    val PaneMotion.exitTransition
-        get() = with(this) { motionDataProvider.exitTransition }
 }
 
 /**
@@ -240,17 +221,11 @@
 }
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal val List<Measurable>.minTouchTargetSize: Dp
-    get() =
-        fastMaxOfOrNull {
-            val size =
-                (it.parentData as? PaneScaffoldParentData)?.minTouchTargetSize ?: Dp.Unspecified
-            if (size == Dp.Unspecified) {
-                0.dp
-            } else {
-                size
-            }
-        } ?: 0.dp
+internal val Measurable.minTouchTargetSize: Dp
+    get() {
+        val size = (parentData as? PaneScaffoldParentData)?.minTouchTargetSize ?: Dp.Unspecified
+        return if (size.isSpecified) size else 0.dp
+    }
 
 /**
  * The parent data passed to pane scaffolds by their contents like panes and drag handles.
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/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index e3477c7..1355351 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -52,7 +52,6 @@
 import androidx.compose.ui.unit.roundToIntRect
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
-import androidx.compose.ui.util.fastMap
 import kotlin.math.max
 import kotlin.math.min
 
@@ -307,10 +306,10 @@
         measurables: List<List<Measurable>>,
         constraints: Constraints
     ): MeasureResult {
-        val primaryMeasurables = measurables[0]
-        val secondaryMeasurables = measurables[1]
-        val tertiaryMeasurables = measurables[2]
-        val dragHandleMeasurables = measurables[3]
+        val primaryMeasurable = measurables[0].firstOrNull()
+        val secondaryMeasurable = measurables[1].firstOrNull()
+        val tertiaryMeasurable = measurables[2].firstOrNull()
+        val dragHandleMeasurable = measurables[3].firstOrNull()
         return layout(constraints.maxWidth, constraints.maxHeight) {
             if (coordinates == null) {
                 return@layout
@@ -319,10 +318,10 @@
             val visiblePanes =
                 getPanesMeasurables(
                     paneOrder = paneOrder,
-                    primaryMeasurables = primaryMeasurables,
+                    primaryMeasurable = primaryMeasurable,
                     scaffoldValue = scaffoldValue,
-                    secondaryMeasurables = secondaryMeasurables,
-                    tertiaryMeasurables = tertiaryMeasurables
+                    secondaryMeasurable = secondaryMeasurable,
+                    tertiaryMeasurable = tertiaryMeasurable
                 ) {
                     it != PaneAdaptedValue.Hidden
                 }
@@ -330,10 +329,10 @@
             val hiddenPanes =
                 getPanesMeasurables(
                     paneOrder = paneOrder,
-                    primaryMeasurables = primaryMeasurables,
+                    primaryMeasurable = primaryMeasurable,
                     scaffoldValue = scaffoldValue,
-                    secondaryMeasurables = secondaryMeasurables,
-                    tertiaryMeasurables = tertiaryMeasurables
+                    secondaryMeasurable = secondaryMeasurable,
+                    tertiaryMeasurable = tertiaryMeasurable
                 ) {
                     it == PaneAdaptedValue.Hidden
                 }
@@ -529,7 +528,7 @@
                 )
             }
 
-            if (visiblePanes.size == 2 && dragHandleMeasurables.isNotEmpty()) {
+            if (visiblePanes.size == 2 && dragHandleMeasurable != null) {
                 val handleOffsetX =
                     if (
                         !paneExpansionState.isDraggingOrSettling ||
@@ -546,11 +545,11 @@
                         paneExpansionState.currentDraggingOffset
                     }
                 measureAndPlaceDragHandleIfNeeded(
-                    measurables = dragHandleMeasurables,
+                    measurable = dragHandleMeasurable,
                     constraints = constraints,
                     contentBounds = outerBounds,
                     minHorizontalMargin = verticalSpacerSize / 2,
-                    minTouchTargetSize = dragHandleMeasurables.minTouchTargetSize.roundToPx(),
+                    minTouchTargetSize = dragHandleMeasurable.minTouchTargetSize.roundToPx(),
                     offsetX = handleOffsetX
                 )
             } else if (!isLookingAhead) {
@@ -568,10 +567,10 @@
     @OptIn(ExperimentalMaterial3AdaptiveApi::class)
     private fun MeasureScope.getPanesMeasurables(
         paneOrder: ThreePaneScaffoldHorizontalOrder,
-        primaryMeasurables: List<Measurable>,
+        primaryMeasurable: Measurable?,
         scaffoldValue: ThreePaneScaffoldValue,
-        secondaryMeasurables: List<Measurable>,
-        tertiaryMeasurables: List<Measurable>,
+        secondaryMeasurable: Measurable?,
+        tertiaryMeasurable: Measurable?,
         predicate: (PaneAdaptedValue) -> Boolean
     ): List<PaneMeasurable> {
         return buildList {
@@ -580,7 +579,7 @@
                     when (role) {
                         ThreePaneScaffoldRole.Primary -> {
                             createPaneMeasurableIfNeeded(
-                                primaryMeasurables,
+                                primaryMeasurable,
                                 ThreePaneScaffoldDefaults.PrimaryPanePriority,
                                 role,
                                 scaffoldDirective.defaultPanePreferredWidth.roundToPx(),
@@ -589,7 +588,7 @@
                         }
                         ThreePaneScaffoldRole.Secondary -> {
                             createPaneMeasurableIfNeeded(
-                                secondaryMeasurables,
+                                secondaryMeasurable,
                                 ThreePaneScaffoldDefaults.SecondaryPanePriority,
                                 role,
                                 scaffoldDirective.defaultPanePreferredWidth.roundToPx(),
@@ -598,7 +597,7 @@
                         }
                         ThreePaneScaffoldRole.Tertiary -> {
                             createPaneMeasurableIfNeeded(
-                                tertiaryMeasurables,
+                                tertiaryMeasurable,
                                 ThreePaneScaffoldDefaults.TertiaryPanePriority,
                                 role,
                                 scaffoldDirective.defaultPanePreferredWidth.roundToPx(),
@@ -612,14 +611,14 @@
     }
 
     private fun MutableList<PaneMeasurable>.createPaneMeasurableIfNeeded(
-        measurables: List<Measurable>,
+        measurable: Measurable?,
         priority: Int,
         role: ThreePaneScaffoldRole,
         defaultPreferredWidth: Int,
         density: Density
     ) {
-        if (measurables.isNotEmpty()) {
-            add(PaneMeasurable(measurables[0], priority, role, defaultPreferredWidth, density))
+        if (measurable != null) {
+            add(PaneMeasurable(measurable, priority, role, defaultPreferredWidth, density))
         }
     }
 
@@ -747,7 +746,7 @@
     }
 
     private fun Placeable.PlacementScope.measureAndPlaceDragHandleIfNeeded(
-        measurables: List<Measurable>,
+        measurable: Measurable,
         constraints: Constraints,
         contentBounds: IntRect,
         minHorizontalMargin: Int,
@@ -778,15 +777,14 @@
             } else {
                 minTouchTargetSize
             }
-        val placeables =
-            measurables.fastMap {
-                it.measure(
-                    Constraints(minWidth = minDragHandleWidth, maxHeight = contentBounds.height)
-                )
-            }
-        placeables.fastForEach {
-            it.place(clampedOffsetX - it.width / 2, (constraints.maxHeight - it.height) / 2)
-        }
+        val placeable =
+            measurable.measure(
+                Constraints(minWidth = minDragHandleWidth, maxHeight = contentBounds.height)
+            )
+        placeable.place(
+            x = clampedOffsetX - placeable.width / 2,
+            y = (constraints.maxHeight - placeable.height) / 2
+        )
     }
 
     private fun getSpacerMiddleOffsetX(paneLeft: PaneMeasurable, paneRight: PaneMeasurable): Int {
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/1.1.0-beta01.txt b/compose/material3/adaptive/adaptive-navigation/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..ba0712b
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-navigation/api/1.1.0-beta01.txt
@@ -0,0 +1,55 @@
+// Signature format: 4.0
+package androidx.compose.material3.adaptive.navigation {
+
+  public final class AndroidThreePaneScaffold_androidKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> void NavigableListDetailPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, 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);
+    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);
+  }
+
+  @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
+    field public static final androidx.compose.material3.adaptive.navigation.BackNavigationBehavior.Companion Companion;
+  }
+
+  public static final class BackNavigationBehavior.Companion {
+    method public String getPopLatest();
+    method public String getPopUntilContentChange();
+    method public String getPopUntilCurrentDestinationChange();
+    method public String getPopUntilScaffoldValueChange();
+    property public final String PopLatest;
+    property public final String PopUntilContentChange;
+    property public final String PopUntilCurrentDestinationChange;
+    property public final String PopUntilScaffoldValueChange;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public interface ThreePaneScaffoldNavigator<T> {
+    method public boolean canNavigateBack(optional String backNavigationBehavior);
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? getCurrentDestination();
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective getScaffoldDirective();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState getScaffoldState();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getScaffoldValue();
+    method public boolean isDestinationHistoryAware();
+    method public suspend Object? navigateBack(optional String backNavigationBehavior, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+    method public suspend Object? navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue peekPreviousScaffoldValue(optional String backNavigationBehavior);
+    method public suspend Object? seekBack(optional String backNavigationBehavior, optional @FloatRange(from=0.0, to=1.0) float fraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public void setDestinationHistoryAware(boolean);
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? currentDestination;
+    property public abstract boolean isDestinationHistoryAware;
+    property public abstract androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective;
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState;
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue scaffoldValue;
+  }
+
+  public final class ThreePaneScaffoldNavigatorKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends T>> initialDestinationHistory);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends T>> initialDestinationHistory);
+  }
+
+  public final class ThreePaneScaffoldPredictiveBackHandler_androidKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> void ThreePaneScaffoldPredictiveBackHandler(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> navigator, String backBehavior);
+  }
+
+}
+
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/res-1.1.0-beta01.txt b/compose/material3/adaptive/adaptive-navigation/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-navigation/api/res-1.1.0-beta01.txt
diff --git a/compose/material3/adaptive/adaptive-navigation/api/restricted_1.1.0-beta01.txt b/compose/material3/adaptive/adaptive-navigation/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..ba0712b
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-navigation/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,55 @@
+// Signature format: 4.0
+package androidx.compose.material3.adaptive.navigation {
+
+  public final class AndroidThreePaneScaffold_androidKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> void NavigableListDetailPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, 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);
+    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);
+  }
+
+  @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
+    field public static final androidx.compose.material3.adaptive.navigation.BackNavigationBehavior.Companion Companion;
+  }
+
+  public static final class BackNavigationBehavior.Companion {
+    method public String getPopLatest();
+    method public String getPopUntilContentChange();
+    method public String getPopUntilCurrentDestinationChange();
+    method public String getPopUntilScaffoldValueChange();
+    property public final String PopLatest;
+    property public final String PopUntilContentChange;
+    property public final String PopUntilCurrentDestinationChange;
+    property public final String PopUntilScaffoldValueChange;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public interface ThreePaneScaffoldNavigator<T> {
+    method public boolean canNavigateBack(optional String backNavigationBehavior);
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? getCurrentDestination();
+    method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective getScaffoldDirective();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState getScaffoldState();
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getScaffoldValue();
+    method public boolean isDestinationHistoryAware();
+    method public suspend Object? navigateBack(optional String backNavigationBehavior, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+    method public suspend Object? navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue peekPreviousScaffoldValue(optional String backNavigationBehavior);
+    method public suspend Object? seekBack(optional String backNavigationBehavior, optional @FloatRange(from=0.0, to=1.0) float fraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public void setDestinationHistoryAware(boolean);
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? currentDestination;
+    property public abstract boolean isDestinationHistoryAware;
+    property public abstract androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective;
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState;
+    property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue scaffoldValue;
+  }
+
+  public final class ThreePaneScaffoldNavigatorKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends T>> initialDestinationHistory);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends T>> initialDestinationHistory);
+  }
+
+  public final class ThreePaneScaffoldPredictiveBackHandler_androidKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> void ThreePaneScaffoldPredictiveBackHandler(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> navigator, String backBehavior);
+  }
+
+}
+
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/1.1.0-beta01.txt b/compose/material3/adaptive/adaptive/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..a93216b
--- /dev/null
+++ b/compose/material3/adaptive/adaptive/api/1.1.0-beta01.txt
@@ -0,0 +1,62 @@
+// Signature format: 4.0
+package androidx.compose.material3.adaptive {
+
+  public final class AndroidPosture_androidKt {
+    method public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
+  }
+
+  public final class AndroidWindowAdaptiveInfo_androidKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<java.util.List<androidx.window.layout.FoldingFeature>> collectFoldingFeaturesAsState();
+    method @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.WindowAdaptiveInfo currentWindowAdaptiveInfo();
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static long currentWindowDpSize();
+    method @androidx.compose.runtime.Composable public static long currentWindowSize();
+  }
+
+  @SuppressCompatibility @kotlin.RequiresOptIn(message="This material3 adaptive API is experimental and is likely to change or to be" + "removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveApi {
+  }
+
+  @SuppressCompatibility @kotlin.RequiresOptIn(message="This material3 adaptive API is experimental and is likely to change or to be" + "removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveComponentOverrideApi {
+  }
+
+  @androidx.compose.runtime.Immutable public final class HingeInfo {
+    ctor public HingeInfo(androidx.compose.ui.geometry.Rect bounds, boolean isFlat, boolean isVertical, boolean isSeparating, boolean isOccluding);
+    method public androidx.compose.ui.geometry.Rect getBounds();
+    method public boolean isFlat();
+    method public boolean isOccluding();
+    method public boolean isSeparating();
+    method public boolean isVertical();
+    property public final androidx.compose.ui.geometry.Rect bounds;
+    property public final boolean isFlat;
+    property public final boolean isOccluding;
+    property public final boolean isSeparating;
+    property public final boolean isVertical;
+  }
+
+  @androidx.compose.runtime.Immutable public final class Posture {
+    ctor public Posture();
+    ctor public Posture(optional boolean isTabletop, optional java.util.List<androidx.compose.material3.adaptive.HingeInfo> hingeList);
+    method public java.util.List<androidx.compose.material3.adaptive.HingeInfo> getHingeList();
+    method public boolean isTabletop();
+    property public final java.util.List<androidx.compose.material3.adaptive.HingeInfo> hingeList;
+    property public final boolean isTabletop;
+  }
+
+  public final class PostureKt {
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getAllHorizontalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getAllVerticalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getOccludingHorizontalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getOccludingVerticalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getSeparatingHorizontalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getSeparatingVerticalHingeBounds(androidx.compose.material3.adaptive.Posture);
+  }
+
+  @androidx.compose.runtime.Immutable public final class WindowAdaptiveInfo {
+    ctor public WindowAdaptiveInfo(androidx.window.core.layout.WindowSizeClass windowSizeClass, androidx.compose.material3.adaptive.Posture windowPosture);
+    method public androidx.compose.material3.adaptive.Posture getWindowPosture();
+    method public androidx.window.core.layout.WindowSizeClass getWindowSizeClass();
+    property public final androidx.compose.material3.adaptive.Posture windowPosture;
+    property public final androidx.window.core.layout.WindowSizeClass windowSizeClass;
+  }
+
+}
+
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/res-1.1.0-beta01.txt b/compose/material3/adaptive/adaptive/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/material3/adaptive/adaptive/api/res-1.1.0-beta01.txt
diff --git a/compose/material3/adaptive/adaptive/api/restricted_1.1.0-beta01.txt b/compose/material3/adaptive/adaptive/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..a93216b
--- /dev/null
+++ b/compose/material3/adaptive/adaptive/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,62 @@
+// Signature format: 4.0
+package androidx.compose.material3.adaptive {
+
+  public final class AndroidPosture_androidKt {
+    method public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
+  }
+
+  public final class AndroidWindowAdaptiveInfo_androidKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<java.util.List<androidx.window.layout.FoldingFeature>> collectFoldingFeaturesAsState();
+    method @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.WindowAdaptiveInfo currentWindowAdaptiveInfo();
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static long currentWindowDpSize();
+    method @androidx.compose.runtime.Composable public static long currentWindowSize();
+  }
+
+  @SuppressCompatibility @kotlin.RequiresOptIn(message="This material3 adaptive API is experimental and is likely to change or to be" + "removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveApi {
+  }
+
+  @SuppressCompatibility @kotlin.RequiresOptIn(message="This material3 adaptive API is experimental and is likely to change or to be" + "removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveComponentOverrideApi {
+  }
+
+  @androidx.compose.runtime.Immutable public final class HingeInfo {
+    ctor public HingeInfo(androidx.compose.ui.geometry.Rect bounds, boolean isFlat, boolean isVertical, boolean isSeparating, boolean isOccluding);
+    method public androidx.compose.ui.geometry.Rect getBounds();
+    method public boolean isFlat();
+    method public boolean isOccluding();
+    method public boolean isSeparating();
+    method public boolean isVertical();
+    property public final androidx.compose.ui.geometry.Rect bounds;
+    property public final boolean isFlat;
+    property public final boolean isOccluding;
+    property public final boolean isSeparating;
+    property public final boolean isVertical;
+  }
+
+  @androidx.compose.runtime.Immutable public final class Posture {
+    ctor public Posture();
+    ctor public Posture(optional boolean isTabletop, optional java.util.List<androidx.compose.material3.adaptive.HingeInfo> hingeList);
+    method public java.util.List<androidx.compose.material3.adaptive.HingeInfo> getHingeList();
+    method public boolean isTabletop();
+    property public final java.util.List<androidx.compose.material3.adaptive.HingeInfo> hingeList;
+    property public final boolean isTabletop;
+  }
+
+  public final class PostureKt {
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getAllHorizontalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getAllVerticalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getOccludingHorizontalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getOccludingVerticalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getSeparatingHorizontalHingeBounds(androidx.compose.material3.adaptive.Posture);
+    method public static java.util.List<androidx.compose.ui.geometry.Rect> getSeparatingVerticalHingeBounds(androidx.compose.material3.adaptive.Posture);
+  }
+
+  @androidx.compose.runtime.Immutable public final class WindowAdaptiveInfo {
+    ctor public WindowAdaptiveInfo(androidx.window.core.layout.WindowSizeClass windowSizeClass, androidx.compose.material3.adaptive.Posture windowPosture);
+    method public androidx.compose.material3.adaptive.Posture getWindowPosture();
+    method public androidx.window.core.layout.WindowSizeClass getWindowSizeClass();
+    property public final androidx.compose.material3.adaptive.Posture windowPosture;
+    property public final androidx.window.core.layout.WindowSizeClass windowSizeClass;
+  }
+
+}
+
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/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/BottomAppBarBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/BottomAppBarBenchmark.kt
index 96f3a4c..a589092 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/BottomAppBarBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/BottomAppBarBenchmark.kt
@@ -21,7 +21,6 @@
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.filled.ArrowForward
 import androidx.compose.material3.BottomAppBar
-import androidx.compose.material3.BottomAppBarDefaults
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
@@ -80,7 +79,6 @@
     @Composable
     override fun MeasuredContent() {
         BottomAppBar(
-            horizontalArrangement = BottomAppBarDefaults.HorizontalArrangement,
             modifier = Modifier.fillMaxWidth(),
         ) {
             IconButton(onClick = { /* doSomething() */ }) {
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index a3501ba..48ebe5e 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -34,7 +34,6 @@
   }
 
   public final class AppBarKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void BottomAppBar(androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
@@ -42,6 +41,7 @@
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void FlexibleBottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LargeFlexibleTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Alignment.Horizontal titleHorizontalAlignment, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
@@ -91,11 +91,17 @@
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method public androidx.compose.foundation.layout.Arrangement.Horizontal getHorizontalArrangement();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getFlexibleBottomAppBarHeight();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.foundation.layout.PaddingValues getFlexibleContentPadding();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.foundation.layout.Arrangement.Horizontal getFlexibleFixedHorizontalArrangement();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.foundation.layout.Arrangement.Horizontal getFlexibleHorizontalArrangement();
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
     property public final float ContainerElevation;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property public final androidx.compose.foundation.layout.Arrangement.Horizontal HorizontalArrangement;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float FlexibleBottomAppBarHeight;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.foundation.layout.PaddingValues FlexibleContentPadding;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.foundation.layout.Arrangement.Horizontal FlexibleFixedHorizontalArrangement;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.foundation.layout.Arrangement.Horizontal FlexibleHorizontalArrangement;
     property @androidx.compose.runtime.Composable public final long bottomAppBarFabColor;
     property @androidx.compose.runtime.Composable public final long containerColor;
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
@@ -211,6 +217,7 @@
     method public float getMinWidth();
     method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getOutlinedButtonBorder();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getOutlinedShape();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getPressedShape();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method public androidx.compose.foundation.layout.PaddingValues getSmallButtonContentPadding();
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSquareShape();
@@ -228,6 +235,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedButtonBorder(optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors outlinedButtonColors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors outlinedButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonShapes shapes();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     property public final androidx.compose.foundation.layout.PaddingValues ButtonWithIconContentPadding;
@@ -259,6 +267,7 @@
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledTonalShape;
     property @Deprecated @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedButtonBorder;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape pressedShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape squareShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape textShape;
@@ -269,25 +278,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 {
@@ -295,20 +304,23 @@
   }
 
   public final class ButtonKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ElevatedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void ElevatedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  public final class ButtonShapes {
-    ctor public ButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
-    method public androidx.compose.material3.ButtonShapes copy(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape, optional androidx.compose.ui.graphics.Shape? checkedShape);
-    method public androidx.compose.ui.graphics.Shape getCheckedShape();
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ButtonShapes {
+    ctor public ButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape);
+    method public androidx.compose.material3.ButtonShapes copy(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape);
     method public androidx.compose.ui.graphics.Shape getPressedShape();
     method public androidx.compose.ui.graphics.Shape getShape();
-    property public final androidx.compose.ui.graphics.Shape checkedShape;
     property public final androidx.compose.ui.graphics.Shape pressedShape;
     property public final androidx.compose.ui.graphics.Shape shape;
   }
@@ -2206,10 +2218,12 @@
     method public int getSteps();
     method public float getValue();
     method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getValueRange();
+    method public boolean isDragging();
     method public void setOnValueChange(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>?);
     method public void setOnValueChangeFinished(kotlin.jvm.functions.Function0<kotlin.Unit>?);
     method public void setValue(float);
     property public final float coercedValueAsFraction;
+    property public final boolean isDragging;
     property public final kotlin.jvm.functions.Function1<java.lang.Float,kotlin.Unit>? onValueChange;
     property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished;
     property @IntRange(from=0L) public final int steps;
@@ -2787,8 +2801,8 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXSmallSquareShape();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors outlinedToggleButtonColors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors outlinedToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonShapes shapes();
-    method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonShapes shapes(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape, optional androidx.compose.ui.graphics.Shape? checkedShape);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonShapes shapes();
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonShapes shapes(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape, optional androidx.compose.ui.graphics.Shape? checkedShape);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors toggleButtonColors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors toggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors tonalToggleButtonColors();
@@ -2818,10 +2832,21 @@
   }
 
   public final class ToggleButtonKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ElevatedToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void OutlinedToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TonalToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ElevatedToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ToggleButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void OutlinedToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ToggleButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ToggleButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TonalToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ToggleButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ToggleButtonShapes {
+    ctor public ToggleButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
+    method public androidx.compose.material3.ToggleButtonShapes copy(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape, optional androidx.compose.ui.graphics.Shape? checkedShape);
+    method public androidx.compose.ui.graphics.Shape getCheckedShape();
+    method public androidx.compose.ui.graphics.Shape getPressedShape();
+    method public androidx.compose.ui.graphics.Shape getShape();
+    property public final androidx.compose.ui.graphics.Shape checkedShape;
+    property public final androidx.compose.ui.graphics.Shape pressedShape;
+    property public final androidx.compose.ui.graphics.Shape shape;
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ToggleFloatingActionButtonDefaults {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index a3501ba..48ebe5e 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -34,7 +34,6 @@
   }
 
   public final class AppBarKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void BottomAppBar(androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
@@ -42,6 +41,7 @@
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void FlexibleBottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LargeFlexibleTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Alignment.Horizontal titleHorizontalAlignment, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
@@ -91,11 +91,17 @@
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method public androidx.compose.foundation.layout.Arrangement.Horizontal getHorizontalArrangement();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getFlexibleBottomAppBarHeight();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.foundation.layout.PaddingValues getFlexibleContentPadding();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.foundation.layout.Arrangement.Horizontal getFlexibleFixedHorizontalArrangement();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.foundation.layout.Arrangement.Horizontal getFlexibleHorizontalArrangement();
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
     property public final float ContainerElevation;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property public final androidx.compose.foundation.layout.Arrangement.Horizontal HorizontalArrangement;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float FlexibleBottomAppBarHeight;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.foundation.layout.PaddingValues FlexibleContentPadding;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.foundation.layout.Arrangement.Horizontal FlexibleFixedHorizontalArrangement;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.foundation.layout.Arrangement.Horizontal FlexibleHorizontalArrangement;
     property @androidx.compose.runtime.Composable public final long bottomAppBarFabColor;
     property @androidx.compose.runtime.Composable public final long containerColor;
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
@@ -211,6 +217,7 @@
     method public float getMinWidth();
     method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getOutlinedButtonBorder();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getOutlinedShape();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getPressedShape();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method public androidx.compose.foundation.layout.PaddingValues getSmallButtonContentPadding();
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSquareShape();
@@ -228,6 +235,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedButtonBorder(optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors outlinedButtonColors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors outlinedButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonShapes shapes();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     property public final androidx.compose.foundation.layout.PaddingValues ButtonWithIconContentPadding;
@@ -259,6 +267,7 @@
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledTonalShape;
     property @Deprecated @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedButtonBorder;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape pressedShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape squareShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape textShape;
@@ -269,25 +278,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 {
@@ -295,20 +304,23 @@
   }
 
   public final class ButtonKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ElevatedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void ElevatedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  public final class ButtonShapes {
-    ctor public ButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
-    method public androidx.compose.material3.ButtonShapes copy(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape, optional androidx.compose.ui.graphics.Shape? checkedShape);
-    method public androidx.compose.ui.graphics.Shape getCheckedShape();
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ButtonShapes {
+    ctor public ButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape);
+    method public androidx.compose.material3.ButtonShapes copy(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape);
     method public androidx.compose.ui.graphics.Shape getPressedShape();
     method public androidx.compose.ui.graphics.Shape getShape();
-    property public final androidx.compose.ui.graphics.Shape checkedShape;
     property public final androidx.compose.ui.graphics.Shape pressedShape;
     property public final androidx.compose.ui.graphics.Shape shape;
   }
@@ -2206,10 +2218,12 @@
     method public int getSteps();
     method public float getValue();
     method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getValueRange();
+    method public boolean isDragging();
     method public void setOnValueChange(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>?);
     method public void setOnValueChangeFinished(kotlin.jvm.functions.Function0<kotlin.Unit>?);
     method public void setValue(float);
     property public final float coercedValueAsFraction;
+    property public final boolean isDragging;
     property public final kotlin.jvm.functions.Function1<java.lang.Float,kotlin.Unit>? onValueChange;
     property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished;
     property @IntRange(from=0L) public final int steps;
@@ -2787,8 +2801,8 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXSmallSquareShape();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors outlinedToggleButtonColors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors outlinedToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonShapes shapes();
-    method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonShapes shapes(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape, optional androidx.compose.ui.graphics.Shape? checkedShape);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonShapes shapes();
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonShapes shapes(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape, optional androidx.compose.ui.graphics.Shape? checkedShape);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors toggleButtonColors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors toggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors tonalToggleButtonColors();
@@ -2818,10 +2832,21 @@
   }
 
   public final class ToggleButtonKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ElevatedToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void OutlinedToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TonalToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ElevatedToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ToggleButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void OutlinedToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ToggleButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ToggleButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TonalToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.ToggleButtonShapes shapes, optional androidx.compose.material3.ToggleButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ToggleButtonShapes {
+    ctor public ToggleButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
+    method public androidx.compose.material3.ToggleButtonShapes copy(optional androidx.compose.ui.graphics.Shape? shape, optional androidx.compose.ui.graphics.Shape? pressedShape, optional androidx.compose.ui.graphics.Shape? checkedShape);
+    method public androidx.compose.ui.graphics.Shape getCheckedShape();
+    method public androidx.compose.ui.graphics.Shape getPressedShape();
+    method public androidx.compose.ui.graphics.Shape getShape();
+    property public final androidx.compose.ui.graphics.Shape checkedShape;
+    property public final androidx.compose.ui.graphics.Shape pressedShape;
+    property public final androidx.compose.ui.graphics.Shape shape;
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ToggleFloatingActionButtonDefaults {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 6d3b4e5..6ab1a76 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -38,6 +38,7 @@
 import androidx.compose.material3.samples.BottomSheetScaffoldNestedScrollSample
 import androidx.compose.material3.samples.ButtonGroupSample
 import androidx.compose.material3.samples.ButtonSample
+import androidx.compose.material3.samples.ButtonWithAnimatedShapeSample
 import androidx.compose.material3.samples.ButtonWithIconSample
 import androidx.compose.material3.samples.CardSample
 import androidx.compose.material3.samples.CenteredHorizontalFloatingToolbarWithFabSample
@@ -67,6 +68,7 @@
 import androidx.compose.material3.samples.EditableExposedDropdownMenuSample
 import androidx.compose.material3.samples.ElevatedAssistChipSample
 import androidx.compose.material3.samples.ElevatedButtonSample
+import androidx.compose.material3.samples.ElevatedButtonWithAnimatedShapeSample
 import androidx.compose.material3.samples.ElevatedCardSample
 import androidx.compose.material3.samples.ElevatedFilterChipSample
 import androidx.compose.material3.samples.ElevatedSplitButtonSample
@@ -96,6 +98,7 @@
 import androidx.compose.material3.samples.FilledIconToggleButtonSample
 import androidx.compose.material3.samples.FilledSplitButtonSample
 import androidx.compose.material3.samples.FilledTonalButtonSample
+import androidx.compose.material3.samples.FilledTonalButtonWithAnimatedShapeSample
 import androidx.compose.material3.samples.FilledTonalIconButtonSample
 import androidx.compose.material3.samples.FilledTonalIconToggleButtonSample
 import androidx.compose.material3.samples.FilterChipSample
@@ -148,6 +151,7 @@
 import androidx.compose.material3.samples.NavigationRailWithOnlySelectedLabelsSample
 import androidx.compose.material3.samples.OneLineListItem
 import androidx.compose.material3.samples.OutlinedButtonSample
+import androidx.compose.material3.samples.OutlinedButtonWithAnimatedShapeSample
 import androidx.compose.material3.samples.OutlinedCardSample
 import androidx.compose.material3.samples.OutlinedIconButtonSample
 import androidx.compose.material3.samples.OutlinedIconToggleButtonSample
@@ -225,6 +229,7 @@
 import androidx.compose.material3.samples.TextAndIconTabs
 import androidx.compose.material3.samples.TextArea
 import androidx.compose.material3.samples.TextButtonSample
+import androidx.compose.material3.samples.TextButtonWithAnimatedShapeSample
 import androidx.compose.material3.samples.TextFieldWithErrorState
 import androidx.compose.material3.samples.TextFieldWithHideKeyboardOnImeAction
 import androidx.compose.material3.samples.TextFieldWithIcons
@@ -346,6 +351,13 @@
             ButtonSample()
         },
         Example(
+            name = "ButtonWithAnimatedShapeSample",
+            description = ButtonsExampleDescription,
+            sourceUrl = ButtonsExampleSourceUrl,
+        ) {
+            ButtonWithAnimatedShapeSample()
+        },
+        Example(
             name = "SquareButtonSample",
             description = ButtonsExampleDescription,
             sourceUrl = ButtonsExampleSourceUrl,
@@ -367,6 +379,13 @@
             ElevatedButtonSample()
         },
         Example(
+            name = "ElevatedButtonWithAnimatedShapeSample",
+            description = ButtonsExampleDescription,
+            sourceUrl = ButtonsExampleSourceUrl,
+        ) {
+            ElevatedButtonWithAnimatedShapeSample()
+        },
+        Example(
             name = "FilledTonalButtonSample",
             description = ButtonsExampleDescription,
             sourceUrl = ButtonsExampleSourceUrl,
@@ -374,6 +393,13 @@
             FilledTonalButtonSample()
         },
         Example(
+            name = "FilledTonalButtonWithAnimatedShapeSample",
+            description = ButtonsExampleDescription,
+            sourceUrl = ButtonsExampleSourceUrl,
+        ) {
+            FilledTonalButtonWithAnimatedShapeSample()
+        },
+        Example(
             name = "OutlinedButtonSample",
             description = ButtonsExampleDescription,
             sourceUrl = ButtonsExampleSourceUrl,
@@ -381,6 +407,13 @@
             OutlinedButtonSample()
         },
         Example(
+            name = "OutlinedButtonWithAnimatedShapeSample",
+            description = ButtonsExampleDescription,
+            sourceUrl = ButtonsExampleSourceUrl,
+        ) {
+            OutlinedButtonWithAnimatedShapeSample()
+        },
+        Example(
             name = "TextButtonSample",
             description = ButtonsExampleDescription,
             sourceUrl = ButtonsExampleSourceUrl,
@@ -388,6 +421,13 @@
             TextButtonSample()
         },
         Example(
+            name = "TextButtonWithAnimatedShapeSample",
+            description = ButtonsExampleDescription,
+            sourceUrl = ButtonsExampleSourceUrl,
+        ) {
+            TextButtonWithAnimatedShapeSample()
+        },
+        Example(
             name = "ButtonWithIconSample",
             description = ButtonsExampleDescription,
             sourceUrl = ButtonsExampleSourceUrl,
diff --git a/compose/material3/material3/lint-baseline.xml b/compose/material3/material3/lint-baseline.xml
index 36d385f..2c56cb3 100644
--- a/compose/material3/material3/lint-baseline.xml
+++ b/compose/material3/material3/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
+<issues format="6" by="lint 8.9.0-alpha06" type="baseline" client="gradle" dependencies="false" name="AGP (8.9.0-alpha06)" variant="all" version="8.9.0-alpha06">
 
     <issue
         id="BanThreadSleep"
@@ -52,6 +52,96 @@
         errorLine1="        Thread.sleep(300)"
         errorLine2="               ~~~~~">
         <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
+            file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt"/>
+    </issue>
+
+    <issue
+        id="BanThreadSleep"
+        message="Uses Thread.sleep()"
+        errorLine1="        Thread.sleep(300)"
+        errorLine2="               ~~~~~">
+        <location
             file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/CardScreenshotTest.kt"/>
     </issue>
 
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
index 3c08716..1986cce 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
@@ -44,6 +44,7 @@
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.FabPosition
 import androidx.compose.material3.FilledIconButton
+import androidx.compose.material3.FlexibleBottomAppBar
 import androidx.compose.material3.FloatingActionButton
 import androidx.compose.material3.FloatingActionButtonDefaults
 import androidx.compose.material3.Icon
@@ -784,8 +785,8 @@
 }
 
 /**
- * A sample for a [BottomAppBar] that collapses when the content is scrolled up, and appears when
- * the content scrolled down. The content is spaced around.
+ * A sample for a [FlexibleBottomAppBar] that collapses when the content is scrolled up, and appears
+ * when the content scrolled down. The content is spaced around.
  */
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Preview
@@ -801,7 +802,7 @@
     Scaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         bottomBar = {
-            BottomAppBar(
+            FlexibleBottomAppBar(
                 horizontalArrangement = Arrangement.SpaceAround,
                 contentPadding = PaddingValues(horizontal = 0.dp),
                 scrollBehavior = if (!isTouchExplorationEnabled) scrollBehavior else null,
@@ -852,8 +853,8 @@
 }
 
 /**
- * A sample for a [BottomAppBar] that collapses when the content is scrolled up, and appears when
- * the content scrolled down. The content is spaced between.
+ * A sample for a [FlexibleBottomAppBar] that collapses when the content is scrolled up, and appears
+ * when the content scrolled down. The content is spaced between.
  */
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Preview
@@ -869,7 +870,7 @@
     Scaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         bottomBar = {
-            BottomAppBar(
+            FlexibleBottomAppBar(
                 horizontalArrangement = Arrangement.SpaceBetween,
                 scrollBehavior = if (!isTouchExplorationEnabled) scrollBehavior else null,
                 content = {
@@ -919,8 +920,8 @@
 }
 
 /**
- * A sample for a [BottomAppBar] that collapses when the content is scrolled up, and appears when
- * the content scrolled down. The content is spaced evenly.
+ * A sample for a [FlexibleBottomAppBar] that collapses when the content is scrolled up, and appears
+ * when the content scrolled down. The content is spaced evenly.
  */
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Preview
@@ -936,7 +937,7 @@
     Scaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         bottomBar = {
-            BottomAppBar(
+            FlexibleBottomAppBar(
                 horizontalArrangement = Arrangement.SpaceEvenly,
                 contentPadding = PaddingValues(horizontal = 0.dp),
                 scrollBehavior = if (!isTouchExplorationEnabled) scrollBehavior else null,
@@ -987,8 +988,8 @@
 }
 
 /**
- * A sample for a [BottomAppBar] that collapses when the content is scrolled up, and appears when
- * the content scrolled down. The content arrangement is fixed.
+ * A sample for a [FlexibleBottomAppBar] that collapses when the content is scrolled up, and appears
+ * when the content scrolled down. The content arrangement is fixed.
  */
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Preview
@@ -1004,8 +1005,8 @@
     Scaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         bottomBar = {
-            BottomAppBar(
-                horizontalArrangement = BottomAppBarDefaults.HorizontalArrangement,
+            FlexibleBottomAppBar(
+                horizontalArrangement = BottomAppBarDefaults.FlexibleFixedHorizontalArrangement,
                 scrollBehavior = if (!isTouchExplorationEnabled) scrollBehavior else null,
                 content = {
                     IconButton(onClick = { /* doSomething() */ }) {
@@ -1054,8 +1055,8 @@
 }
 
 /**
- * A sample for a vibrant [BottomAppBar] that collapses when the content is scrolled up, and appears
- * when the content scrolled down. The content arrangement is fixed.
+ * A sample for a vibrant [FlexibleBottomAppBar] that collapses when the content is scrolled up, and
+ * appears when the content scrolled down. The content arrangement is fixed.
  */
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Preview
@@ -1071,8 +1072,8 @@
     Scaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         bottomBar = {
-            BottomAppBar(
-                horizontalArrangement = BottomAppBarDefaults.HorizontalArrangement,
+            FlexibleBottomAppBar(
+                horizontalArrangement = BottomAppBarDefaults.FlexibleFixedHorizontalArrangement,
                 scrollBehavior = if (!isTouchExplorationEnabled) scrollBehavior else null,
                 containerColor =
                     MaterialTheme.colorScheme.primaryContainer, // TODO(b/356885344): tokens
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 d9aa02d..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
@@ -31,13 +31,13 @@
 import androidx.compose.material.icons.outlined.Work
 import androidx.compose.material3.ButtonGroup
 import androidx.compose.material3.ButtonGroupDefaults
-import androidx.compose.material3.ButtonShapes
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
 import androidx.compose.material3.ShapeDefaults
 import androidx.compose.material3.Text
 import androidx.compose.material3.ToggleButton
 import androidx.compose.material3.ToggleButtonDefaults
+import androidx.compose.material3.ToggleButtonShapes
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
@@ -81,19 +81,19 @@
 @Composable
 fun SingleSelectConnectedButtonGroupSample() {
     val startButtonShapes =
-        ButtonShapes(
+        ToggleButtonShapes(
             shape = ButtonGroupDefaults.connectedLeadingButtonShape,
             pressedShape = ButtonGroupDefaults.connectedLeadingButtonPressShape,
             checkedShape = ToggleButtonDefaults.checkedShape
         )
     val middleButtonShapes =
-        ButtonShapes(
+        ToggleButtonShapes(
             shape = ShapeDefaults.Small,
             pressedShape = ToggleButtonDefaults.pressedShape,
             checkedShape = ToggleButtonDefaults.checkedShape
         )
     val endButtonShapes =
-        ButtonShapes(
+        ToggleButtonShapes(
             shape = ButtonGroupDefaults.connectedTrailingButtonShape,
             pressedShape = ButtonGroupDefaults.connectedTrailingButtonPressShape,
             checkedShape = ToggleButtonDefaults.checkedShape
@@ -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(
@@ -132,19 +132,19 @@
 @Composable
 fun MultiSelectConnectedButtonGroupSample() {
     val startButtonShapes =
-        ButtonShapes(
+        ToggleButtonShapes(
             shape = ButtonGroupDefaults.connectedLeadingButtonShape,
             pressedShape = ButtonGroupDefaults.connectedLeadingButtonPressShape,
             checkedShape = ToggleButtonDefaults.checkedShape
         )
     val middleButtonShapes =
-        ButtonShapes(
+        ToggleButtonShapes(
             shape = ShapeDefaults.Small,
             pressedShape = ToggleButtonDefaults.pressedShape,
             checkedShape = ToggleButtonDefaults.checkedShape
         )
     val endButtonShapes =
-        ButtonShapes(
+        ToggleButtonShapes(
             shape = ButtonGroupDefaults.connectedTrailingButtonShape,
             pressedShape = ButtonGroupDefaults.connectedTrailingButtonPressShape,
             checkedShape = ToggleButtonDefaults.checkedShape
@@ -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/samples/src/main/java/androidx/compose/material3/samples/ButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonSamples.kt
index 3b04b270..3f92b08 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonSamples.kt
@@ -41,7 +41,15 @@
 @Sampled
 @Composable
 fun ButtonSample() {
-    Button(onClick = { /* Do something! */ }) { Text("Button") }
+    Button(onClick = {}) { Text("Button") }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun ButtonWithAnimatedShapeSample() {
+    Button(onClick = {}, shapes = ButtonDefaults.shapes()) { Text("Button") }
 }
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -72,6 +80,14 @@
     ElevatedButton(onClick = { /* Do something! */ }) { Text("Elevated Button") }
 }
 
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun ElevatedButtonWithAnimatedShapeSample() {
+    ElevatedButton(onClick = {}, shapes = ButtonDefaults.shapes()) { Text("Elevated Button") }
+}
+
 @Preview
 @Sampled
 @Composable
@@ -79,6 +95,16 @@
     FilledTonalButton(onClick = { /* Do something! */ }) { Text("Filled Tonal Button") }
 }
 
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun FilledTonalButtonWithAnimatedShapeSample() {
+    FilledTonalButton(onClick = {}, shapes = ButtonDefaults.shapes()) {
+        Text("Filled Tonal Button")
+    }
+}
+
 @Preview
 @Sampled
 @Composable
@@ -86,6 +112,14 @@
     OutlinedButton(onClick = { /* Do something! */ }) { Text("Outlined Button") }
 }
 
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun OutlinedButtonWithAnimatedShapeSample() {
+    OutlinedButton(onClick = {}, shapes = ButtonDefaults.shapes()) { Text("Outlined Button") }
+}
+
 @Preview
 @Sampled
 @Composable
@@ -93,6 +127,14 @@
     TextButton(onClick = { /* Do something! */ }) { Text("Text Button") }
 }
 
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun TextButtonWithAnimatedShapeSample() {
+    TextButton(onClick = {}, shapes = ButtonDefaults.shapes()) { Text("Text Button") }
+}
+
 @Preview
 @Sampled
 @Composable
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
index 5a8230df..a71addb 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material3.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.animation.core.animate
 import androidx.compose.foundation.indication
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Column
@@ -39,6 +40,7 @@
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Label
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.PlainTooltip
 import androidx.compose.material3.RangeSlider
 import androidx.compose.material3.RangeSliderState
@@ -50,8 +52,10 @@
 import androidx.compose.material3.ripple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -69,6 +73,8 @@
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import kotlin.math.roundToInt
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
 
 @Preview
 @Sampled
@@ -308,6 +314,7 @@
 @Sampled
 @Composable
 fun VerticalSliderSample() {
+    val coroutineScope = rememberCoroutineScope()
     val sliderState = remember {
         SliderState(
             valueRange = 0f..100f,
@@ -317,6 +324,29 @@
             }
         )
     }
+    val snapAnimationSpec = MaterialTheme.motionScheme.fastEffectsSpec<Float>()
+    var currentValue by remember { mutableFloatStateOf(sliderState.value) }
+    var animateJob: Job? by remember { mutableStateOf(null) }
+    sliderState.onValueChange = { newValue ->
+        currentValue = newValue
+        // only update the sliderState instantly if dragging
+        if (sliderState.isDragging) {
+            animateJob?.cancel()
+            sliderState.value = newValue
+        }
+    }
+    sliderState.onValueChangeFinished = {
+        animateJob =
+            coroutineScope.launch {
+                animate(
+                    initialValue = sliderState.value,
+                    targetValue = currentValue,
+                    animationSpec = snapAnimationSpec
+                ) { value, _ ->
+                    sliderState.value = value
+                }
+            }
+    }
     val interactionSource = remember { MutableInteractionSource() }
     Column(modifier = Modifier.padding(horizontal = 16.dp)) {
         Text(
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt
index 98b620c..b051fcd 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt
@@ -222,7 +222,6 @@
             tonalElevation = 6.dp,
             modifier =
                 Modifier.width(IntrinsicSize.Min)
-                    .height(IntrinsicSize.Min)
                     .background(
                         shape = MaterialTheme.shapes.extraLarge,
                         color = MaterialTheme.colorScheme.surface
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ToggleButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ToggleButtonSamples.kt
index 5006587..49b797d 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ToggleButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ToggleButtonSamples.kt
@@ -24,7 +24,6 @@
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.outlined.Edit
 import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ButtonShapes
 import androidx.compose.material3.ElevatedToggleButton
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
@@ -33,6 +32,7 @@
 import androidx.compose.material3.Text
 import androidx.compose.material3.ToggleButton
 import androidx.compose.material3.ToggleButtonDefaults
+import androidx.compose.material3.ToggleButtonShapes
 import androidx.compose.material3.TonalToggleButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -58,7 +58,7 @@
 fun SquareToggleButtonSample() {
     var checked by remember { mutableStateOf(false) }
     val shapes =
-        ButtonShapes(
+        ToggleButtonShapes(
             shape = ToggleButtonDefaults.squareShape,
             pressedShape = ToggleButtonDefaults.pressedShape,
             checkedShape = ToggleButtonDefaults.roundShape
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index 1a9d7b6..4b0b977 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -657,7 +657,7 @@
     fun bottomAppBarSpacedAround_lightTheme() {
         composeTestRule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(BottomAppBarTestTag)) {
-                BottomAppBar(
+                FlexibleBottomAppBar(
                     horizontalArrangement = Arrangement.SpaceAround,
                     contentPadding = PaddingValues(horizontal = 0.dp),
                     content = {
@@ -700,8 +700,9 @@
     fun bottomAppBarSpacedBetween_lightTheme() {
         composeTestRule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(BottomAppBarTestTag)) {
-                BottomAppBar(
-                    horizontalArrangement = Arrangement.SpaceBetween,
+                FlexibleBottomAppBar(
+                    // The default BottomAppBarDefaults.FlexibleHorizontalArrangement is an
+                    // Arrangement.SpacedBetween.
                     content = {
                         IconButton(onClick = { /* doSomething() */ }) {
                             Icon(
@@ -742,7 +743,7 @@
     fun bottomAppBarSpacedEvenly_lightTheme() {
         composeTestRule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(BottomAppBarTestTag)) {
-                BottomAppBar(
+                FlexibleBottomAppBar(
                     horizontalArrangement = Arrangement.SpaceEvenly,
                     contentPadding = PaddingValues(horizontal = 0.dp),
                     content = {
@@ -785,8 +786,8 @@
     fun bottomAppBarFixed_lightTheme() {
         composeTestRule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(BottomAppBarTestTag)) {
-                BottomAppBar(
-                    horizontalArrangement = BottomAppBarDefaults.HorizontalArrangement,
+                FlexibleBottomAppBar(
+                    horizontalArrangement = BottomAppBarDefaults.FlexibleFixedHorizontalArrangement,
                     content = {
                         IconButton(onClick = { /* doSomething() */ }) {
                             Icon(
@@ -827,8 +828,8 @@
     fun bottomAppBarFixed_darkTheme() {
         composeTestRule.setMaterialContent(darkColorScheme()) {
             Box(Modifier.testTag(BottomAppBarTestTag)) {
-                BottomAppBar(
-                    horizontalArrangement = BottomAppBarDefaults.HorizontalArrangement,
+                FlexibleBottomAppBar(
+                    horizontalArrangement = BottomAppBarDefaults.FlexibleFixedHorizontalArrangement,
                     content = {
                         IconButton(onClick = { /* doSomething() */ }) {
                             Icon(
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
index 71612d1..94eb5d0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -1852,12 +1852,27 @@
     fun bottomAppBarWithCustomArrangement_heightIsFromSpec() {
         rule
             .setMaterialContentForSizeAssertions {
-                BottomAppBar(
-                    horizontalArrangement = BottomAppBarDefaults.HorizontalArrangement,
+                FlexibleBottomAppBar(
+                    horizontalArrangement = BottomAppBarDefaults.FlexibleFixedHorizontalArrangement,
                     content = {}
                 )
             }
-            .assertHeightIsEqualTo(64.dp) // TODO tokens
+            .assertHeightIsEqualTo(BottomAppBarDefaults.FlexibleBottomAppBarHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun bottomAppBarWithCustomHeight() {
+        val height = 128.dp
+        rule
+            .setMaterialContentForSizeAssertions {
+                FlexibleBottomAppBar(
+                    horizontalArrangement = BottomAppBarDefaults.FlexibleFixedHorizontalArrangement,
+                    expandedHeight = height,
+                    content = {}
+                )
+            }
+            .assertHeightIsEqualTo(height)
             .assertWidthIsEqualTo(rule.rootWidth())
     }
 
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/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
index c192fc5..4f2f782 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -49,6 +50,8 @@
 
     @get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
 
+    private val buttonTestTag = "button"
+
     @Test
     fun default_button_light_theme() {
         rule.setMaterialContent(lightColorScheme()) { Button(onClick = {}) { Text("Button") } }
@@ -143,14 +146,14 @@
             FilledTonalButton(
                 onClick = {},
                 enabled = false,
-                modifier = Modifier.testTag("button")
+                modifier = Modifier.testTag(buttonTestTag)
             ) {
                 Text("Filled tonal Button")
             }
         }
 
         rule
-            .onNodeWithTag("button")
+            .onNodeWithTag(buttonTestTag)
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "filled_tonal_button_disabled_light_theme")
     }
@@ -170,13 +173,17 @@
     @Test
     fun disabled_outlined_button_lightTheme() {
         rule.setMaterialContent(lightColorScheme()) {
-            OutlinedButton(onClick = {}, enabled = false, modifier = Modifier.testTag("button")) {
+            OutlinedButton(
+                onClick = {},
+                enabled = false,
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
                 Text("Outlined Button")
             }
         }
 
         rule
-            .onNodeWithTag("button")
+            .onNodeWithTag(buttonTestTag)
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "outlined_button_disabled_light_theme")
     }
@@ -196,13 +203,13 @@
     @Test
     fun disabled_text_button_lightTheme() {
         rule.setMaterialContent(lightColorScheme()) {
-            TextButton(onClick = {}, enabled = false, modifier = Modifier.testTag("button")) {
+            TextButton(onClick = {}, enabled = false, modifier = Modifier.testTag(buttonTestTag)) {
                 Text("Text Button")
             }
         }
 
         rule
-            .onNodeWithTag("button")
+            .onNodeWithTag(buttonTestTag)
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "text_button_disabled_light_theme")
     }
@@ -237,7 +244,7 @@
                 onClick = { /* Do something! */ },
                 contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
                 enabled = false,
-                modifier = Modifier.testTag("button")
+                modifier = Modifier.testTag(buttonTestTag)
             ) {
                 Icon(
                     Icons.Filled.Favorite,
@@ -250,7 +257,7 @@
         }
 
         rule
-            .onNodeWithTag("button")
+            .onNodeWithTag(buttonTestTag)
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "button_withIcon_disabled_lightTheme")
     }
@@ -285,7 +292,7 @@
                 onClick = { /* Do something! */ },
                 contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
                 enabled = false,
-                modifier = Modifier.testTag("button")
+                modifier = Modifier.testTag(buttonTestTag)
             ) {
                 Icon(
                     Icons.Filled.Favorite,
@@ -298,8 +305,534 @@
         }
 
         rule
-            .onNodeWithTag("button")
+            .onNodeWithTag(buttonTestTag)
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "button_withIcon_disabled_darkTheme")
     }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun button_withAnimatedShape_default_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Button(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "button_withAnimatedShape_default_lightTheme")
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun button_withAnimatedShape_default_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Button(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "button_withAnimatedShape_default_darkTheme")
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun elevatedButton_withAnimatedShape_default_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            ElevatedButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "elevatedButton_withAnimatedShape_default_lightTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun elevatedButton_withAnimatedShape_default_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            ElevatedButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "elevatedButton_withAnimatedShape_default_darkTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun filledTonalButton_withAnimatedShape_default_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            FilledTonalButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "filledTonalButton_withAnimatedShape_default_lightTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun filledTonalButton_withAnimatedShape_default_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Button(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "filledTonalButton_withAnimatedShape_default_darkTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun outlinedButton_withAnimatedShape_default_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            OutlinedButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "outlinedButton_withAnimatedShape_default_lightTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun outlinedButton_withAnimatedShape_default_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            OutlinedButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "outlinedButton_withAnimatedShape_default_darkTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun textButton_withAnimatedShape_default_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            TextButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "textButton_withAnimatedShape_default_lightTheme")
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun textButton_withAnimatedShape_default_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            TextButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "textButton_withAnimatedShape_default_darkTheme")
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun button_withAnimatedShape_pressed_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Button(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "button_withAnimatedShape_pressed_lightTheme")
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun button_withAnimatedShape_pressed_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Button(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "button_withAnimatedShape_pressed_darkTheme")
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun elevatedButton_withAnimatedShape_pressed_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            ElevatedButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "elevatedButton_withAnimatedShape_pressed_lightTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun elevatedButton_withAnimatedShape_pressed_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            ElevatedButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "elevatedButton_withAnimatedShape_pressed_darkTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun filledTonalButton_withAnimatedShape_pressed_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            FilledTonalButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "filledTonalButton_withAnimatedShape_pressed_lightTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun filledTonalButton_withAnimatedShape_pressed_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            Button(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "filledTonalButton_withAnimatedShape_pressed_darkTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun outlinedButton_withAnimatedShape_pressed_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            OutlinedButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "outlinedButton_withAnimatedShape_pressed_lightTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun outlinedButton_withAnimatedShape_pressed_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            OutlinedButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "outlinedButton_withAnimatedShape_pressed_darkTheme"
+            )
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun textButton_withAnimatedShape_pressed_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            TextButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "textButton_withAnimatedShape_pressed_lightTheme")
+    }
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun textButton_withAnimatedShape_pressed_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            TextButton(
+                onClick = {},
+                shapes = ButtonDefaults.shapes(),
+                modifier = Modifier.testTag(buttonTestTag)
+            ) {
+                Text("Button")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(buttonTestTag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        rule
+            .onNodeWithTag(buttonTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "textButton_withAnimatedShape_pressed_darkTheme")
+    }
 }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
index 1cac542..acca58b 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
@@ -15,6 +15,7 @@
  */
 package androidx.compose.material3
 
+import android.os.Build
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
@@ -25,7 +26,6 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.outlined.Edit
 import androidx.compose.material3.tokens.ElevatedButtonTokens
 import androidx.compose.material3.tokens.FilledButtonTokens
 import androidx.compose.material3.tokens.FilledTonalButtonTokens
@@ -35,8 +35,10 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
@@ -48,15 +50,18 @@
 import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertIsEqualTo
 import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.height
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -669,6 +674,80 @@
             "height of button"
         )
     }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun button_withAnimatedShape_defaultShape() {
+        lateinit var shape: Shape
+        val backgroundColor = Color.Yellow
+        val shapeColor = Color.Blue
+        rule.setMaterialContent(lightColorScheme()) {
+            shape = ButtonDefaults.shape
+            Surface(color = backgroundColor) {
+                Button(
+                    onClick = {},
+                    shapes = ButtonDefaults.shapes(),
+                    modifier = Modifier.testTag(ButtonTestTag),
+                    colors =
+                        ButtonDefaults.buttonColors(
+                            containerColor = shapeColor,
+                            contentColor = shapeColor
+                        )
+                ) {
+                    Text("Button")
+                }
+            }
+        }
+
+        rule
+            .onNodeWithTag(ButtonTestTag)
+            .captureToImage()
+            .assertShape(
+                density = rule.density,
+                shapeColor = shapeColor,
+                backgroundColor = backgroundColor,
+                shape = shape
+            )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    @Test
+    fun button_withAnimatedShape_pressedShape() {
+        lateinit var shape: Shape
+        val backgroundColor = Color.Yellow
+        val shapeColor = Color.Blue
+        rule.setMaterialContent(lightColorScheme()) {
+            shape = ButtonDefaults.pressedShape
+            Surface(color = backgroundColor) {
+                Button(
+                    onClick = {},
+                    shapes = ButtonDefaults.shapes(),
+                    modifier = Modifier.testTag(ButtonTestTag),
+                    colors =
+                        ButtonDefaults.buttonColors(
+                            containerColor = shapeColor,
+                            contentColor = shapeColor
+                        )
+                ) {
+                    Text("Button")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ButtonTestTag).performTouchInput { down(center) }
+
+        rule
+            .onNodeWithTag(ButtonTestTag)
+            .captureToImage()
+            .assertShape(
+                density = rule.density,
+                shapeColor = shapeColor,
+                backgroundColor = backgroundColor,
+                shape = shape
+            )
+    }
 }
 
 private const val ButtonTestTag = "button"
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
index cfe85b6..dd20fdc 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
@@ -442,6 +442,227 @@
         assertAgainstGolden("dockedSearchBar_shadow_active")
     }
 
+    @Test
+    fun newSearchBar_collapsed() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            val state = rememberSearchBarState()
+            SearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState(),
+                        onSearch = {},
+                        placeholder = { Text("Hint") },
+                    )
+                },
+            )
+        }
+        assertAgainstGolden("searchBar_collapsed_${scheme.name}")
+    }
+
+    @Test
+    fun newSearchBar_collapsed_shadow() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val state = rememberSearchBarState()
+            SearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState(),
+                        onSearch = {},
+                        placeholder = { Text("Hint") },
+                    )
+                },
+                shadowElevation = 6.dp,
+            )
+        }
+        assertAgainstGolden("searchBar_collapsed_shadow")
+    }
+
+    @Test
+    fun newSearchBar_collapsed_disabled() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            val state = rememberSearchBarState()
+            SearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState(),
+                        onSearch = {},
+                        enabled = false,
+                        placeholder = { Text("Hint") },
+                    )
+                },
+            )
+        }
+        assertAgainstGolden("searchBar_collapsed_disabled_${scheme.name}")
+    }
+
+    @Test
+    fun newSearchBar_fullScreen_expanded() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            val state = rememberSearchBarState(initialExpanded = true)
+            ExpandedFullScreenSearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState("Query"),
+                        onSearch = {},
+                    )
+                },
+                content = { Text("Content") },
+            )
+        }
+        assertAgainstGolden("searchBar_fullScreen_${scheme.name}")
+    }
+
+    @Test
+    fun newSearchBar_fullScreen_expanded_withIcons() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            val state = rememberSearchBarState(initialExpanded = true)
+            ExpandedFullScreenSearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState("Query"),
+                        onSearch = {},
+                        leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+                        trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+                    )
+                },
+                content = { Text("Content") },
+            )
+        }
+        assertAgainstGolden("searchBar_fullScreen_withIcons_${scheme.name}")
+    }
+
+    @Test
+    fun newSearchBar_fullScreen_expanded_customColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val state = rememberSearchBarState(initialExpanded = true)
+            val colors =
+                SearchBarDefaults.colors(
+                    containerColor = Color.Yellow,
+                    dividerColor = Color.Green,
+                )
+            ExpandedFullScreenSearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState("Query"),
+                        onSearch = {},
+                        colors = colors.inputFieldColors,
+                    )
+                },
+                colors = colors,
+                content = { Text("Content") },
+            )
+        }
+        assertAgainstGolden("searchBar_fullScreen_customColors")
+    }
+
+    @Test
+    fun newSearchBar_docked_expanded() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            val state = rememberSearchBarState(initialExpanded = true)
+            ExpandedDockedSearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState("Query"),
+                        onSearch = {},
+                    )
+                },
+                content = { Text("Content") },
+            )
+        }
+        assertAgainstGolden("searchBar_docked_${scheme.name}")
+    }
+
+    @Test
+    fun newSearchBar_docked_expanded_withIcons() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            val state = rememberSearchBarState(initialExpanded = true)
+            ExpandedDockedSearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState("Query"),
+                        onSearch = {},
+                        leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+                        trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+                    )
+                },
+                content = { Text("Content") },
+            )
+        }
+        assertAgainstGolden("searchBar_docked_withIcons_${scheme.name}")
+    }
+
+    @Test
+    fun newSearchBar_docked_expanded_customShape() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val state = rememberSearchBarState(initialExpanded = true)
+            ExpandedDockedSearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState("Query"),
+                        onSearch = {},
+                    )
+                },
+                shape = CutCornerShape(24.dp),
+                content = { Text("Content") },
+            )
+        }
+        assertAgainstGolden("searchBar_docked_customShape")
+    }
+
+    @Test
+    fun newSearchBar_docked_expanded_customColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val state = rememberSearchBarState(initialExpanded = true)
+            val colors =
+                SearchBarDefaults.colors(
+                    containerColor = Color.Yellow,
+                    dividerColor = Color.Green,
+                )
+            ExpandedDockedSearchBar(
+                modifier = Modifier.testTag(testTag),
+                state = state,
+                inputField = {
+                    SearchBarDefaults.InputField(
+                        searchBarState = state,
+                        textFieldState = rememberTextFieldState("Query"),
+                        onSearch = {},
+                        colors = colors.inputFieldColors,
+                    )
+                },
+                colors = colors,
+                content = { Text("Content") },
+            )
+        }
+        assertAgainstGolden("searchBar_docked_customColors")
+    }
+
     private fun assertAgainstGolden(goldenName: String) {
         rule.onNodeWithTag(testTag).captureToImage().assertAgainstGolden(screenshotRule, goldenName)
     }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerScreenshotTest.kt
index e273437..9964472 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerScreenshotTest.kt
@@ -18,8 +18,10 @@
 
 import android.os.Build
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
@@ -28,6 +30,7 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -122,6 +125,52 @@
         rule.assertAgainstGolden("timePicker_24h_rtl_${scheme.name}")
     }
 
+    @Test
+    fun clockFace_24h_mid() {
+        val state =
+            AnalogTimePickerState(
+                TimePickerState(initialHour = 0, initialMinute = 0, is24Hour = true)
+            )
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(
+                modifier = Modifier.testTag(TestTag).size(340.dp),
+                contentAlignment = Alignment.Center
+            ) {
+                ClockFace(
+                    modifier = Modifier.then(ClockFaceSizeModifier()),
+                    state = state,
+                    colors = TimePickerDefaults.colors(),
+                    autoSwitchToMinute = true
+                )
+            }
+        }
+
+        rule.assertAgainstGolden("timePicker_24h_mid${scheme.name}")
+    }
+
+    @Test
+    fun clockFace_24h_min() {
+        val state =
+            AnalogTimePickerState(
+                TimePickerState(initialHour = 0, initialMinute = 0, is24Hour = true)
+            )
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(
+                modifier = Modifier.testTag(TestTag).size(300.dp),
+                contentAlignment = Alignment.Center
+            ) {
+                ClockFace(
+                    modifier = Modifier.then(ClockFaceSizeModifier()),
+                    state = state,
+                    colors = TimePickerDefaults.colors(),
+                    autoSwitchToMinute = true
+                )
+            }
+        }
+
+        rule.assertAgainstGolden("timePicker_24h_min${scheme.name}")
+    }
+
     private fun ComposeContentTestRule.assertAgainstGolden(goldenName: String) {
         this.onNodeWithTag(TestTag).captureToImage().assertAgainstGolden(screenshotRule, goldenName)
     }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerSizeTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerSizeTest.kt
new file mode 100644
index 0000000..7e4cbcc
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerSizeTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalMaterial3Api::class)
+class TimePickerSizeTest(val config: Config) {
+
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun clockFace_size_resizesCorrectly() {
+        val state =
+            AnalogTimePickerState(
+                TimePickerState(initialHour = 10, initialMinute = 23, is24Hour = false)
+            )
+        rule
+            .setMaterialContentForSizeAssertions(
+                parentMaxWidth = config.size.width,
+                parentMaxHeight = config.size.height
+            ) {
+                ClockFace(
+                    modifier = Modifier.then(ClockFaceSizeModifier()),
+                    state = state,
+                    colors = TimePickerDefaults.colors(),
+                    autoSwitchToMinute = true
+                )
+            }
+            .assertIsSquareWithSize(config.expected)
+    }
+
+    @Test
+    fun clockFace_24Hour_everyValue() {
+        val state =
+            AnalogTimePickerState(
+                TimePickerState(initialHour = 10, initialMinute = 23, is24Hour = true)
+            )
+
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.size(config.size)) {
+                ClockFace(
+                    modifier = Modifier,
+                    state = state,
+                    colors = TimePickerDefaults.colors(),
+                    autoSwitchToMinute = true
+                )
+            }
+        }
+
+        repeat(24) { number ->
+            rule
+                .onNodeWithTimeValue(number, TimePickerSelectionMode.Hour, is24Hour = true)
+                .performClick()
+
+            rule.runOnIdle {
+                state.selection = TimePickerSelectionMode.Hour
+                assertThat(state.hour).isEqualTo(number)
+            }
+
+            rule
+                .onNodeWithTimeValue(number, TimePickerSelectionMode.Hour, is24Hour = true)
+                .assertIsSelected()
+        }
+    }
+
+    internal companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() =
+            arrayOf(
+                Config(DpSize(384.dp, 384.dp), 256.dp),
+                Config(DpSize(350.dp, 350.dp), 238.dp),
+                Config(DpSize(300.dp, 300.dp), 200.dp)
+            )
+    }
+
+    data class Config(val size: DpSize, val expected: Dp)
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
index 4fffa57..16b4f65 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
@@ -24,6 +24,7 @@
 import androidx.compose.material3.internal.Strings
 import androidx.compose.material3.internal.getString
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
@@ -664,7 +665,12 @@
             )
 
         rule.setMaterialContent(lightColorScheme()) {
-            ClockFace(state, TimePickerDefaults.colors(), autoSwitchToMinute = true)
+            ClockFace(
+                modifier = Modifier,
+                state = state,
+                colors = TimePickerDefaults.colors(),
+                autoSwitchToMinute = true
+            )
         }
 
         repeat(24) { number ->
@@ -691,7 +697,12 @@
             )
 
         rule.setMaterialContent(lightColorScheme()) {
-            ClockFace(state, TimePickerDefaults.colors(), autoSwitchToMinute = true)
+            ClockFace(
+                modifier = Modifier,
+                state = state,
+                colors = TimePickerDefaults.colors(),
+                autoSwitchToMinute = true
+            )
         }
 
         repeat(12) { number ->
@@ -721,7 +732,12 @@
             )
 
         rule.setMaterialContent(lightColorScheme()) {
-            ClockFace(state, TimePickerDefaults.colors(), autoSwitchToMinute = true)
+            ClockFace(
+                modifier = Modifier,
+                state = state,
+                colors = TimePickerDefaults.colors(),
+                autoSwitchToMinute = true
+            )
         }
 
         repeat(12) { number ->
@@ -763,7 +779,12 @@
             )
         state.selection = TimePickerSelectionMode.Minute
         rule.setMaterialContent(lightColorScheme()) {
-            ClockFace(state, TimePickerDefaults.colors(), autoSwitchToMinute = true)
+            ClockFace(
+                modifier = Modifier,
+                state = state,
+                colors = TimePickerDefaults.colors(),
+                autoSwitchToMinute = true
+            )
         }
 
         repeat(11) { number ->
@@ -782,7 +803,12 @@
             )
         state.selection = TimePickerSelectionMode.Minute
         rule.setMaterialContent(lightColorScheme()) {
-            ClockFace(state, TimePickerDefaults.colors(), autoSwitchToMinute = true)
+            ClockFace(
+                modifier = Modifier,
+                state = state,
+                colors = TimePickerDefaults.colors(),
+                autoSwitchToMinute = true
+            )
         }
 
         repeat(11) { number ->
@@ -790,38 +816,40 @@
             rule.runOnIdle { assertThat(state.minute).isEqualTo(number * 5) }
         }
     }
+}
 
-    private fun contentDescriptionForValue(
-        resources: Resources,
-        selection: TimePickerSelectionMode,
-        is24Hour: Boolean,
-        number: Int
-    ): String {
-
-        val id =
-            if (selection == TimePickerSelectionMode.Minute) {
-                R.string.m3c_time_picker_minute_suffix
-            } else if (is24Hour) {
-                R.string.m3c_time_picker_hour_24h_suffix
-            } else {
-                R.string.m3c_time_picker_hour_suffix
-            }
-
-        return resources.getString(id, number)
-    }
-
-    private fun SemanticsNodeInteractionsProvider.onNodeWithTimeValue(
-        number: Int,
-        selection: TimePickerSelectionMode,
-        is24Hour: Boolean = false,
-    ): SemanticsNodeInteraction =
-        onAllNodesWithContentDescription(
-                contentDescriptionForValue(
-                    InstrumentationRegistry.getInstrumentation().context.resources,
-                    selection,
-                    is24Hour,
-                    number
-                )
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun SemanticsNodeInteractionsProvider.onNodeWithTimeValue(
+    number: Int,
+    selection: TimePickerSelectionMode,
+    is24Hour: Boolean = false,
+): SemanticsNodeInteraction =
+    onAllNodesWithContentDescription(
+            contentDescriptionForValue(
+                InstrumentationRegistry.getInstrumentation().context.resources,
+                selection,
+                is24Hour,
+                number
             )
-            .onFirst()
+        )
+        .onFirst()
+
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun contentDescriptionForValue(
+    resources: Resources,
+    selection: TimePickerSelectionMode,
+    is24Hour: Boolean,
+    number: Int
+): String {
+
+    val id =
+        if (selection == TimePickerSelectionMode.Minute) {
+            R.string.m3c_time_picker_minute_suffix
+        } else if (is24Hour) {
+            R.string.m3c_time_picker_hour_24h_suffix
+        } else {
+            R.string.m3c_time_picker_hour_suffix
+        }
+
+    return resources.getString(id, number)
 }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonTest.kt
index 8257ca35..c4f2f68 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonTest.kt
@@ -310,7 +310,7 @@
     @Test
     fun buttonShapes_AllRounded_hasRoundedShapesIsTrue() {
         assertThat(
-                ButtonShapes(
+                ToggleButtonShapes(
                         shape = RoundedCornerShape(10.dp),
                         pressedShape = RoundedCornerShape(10.dp),
                         checkedShape = RoundedCornerShape(4.dp),
@@ -323,7 +323,7 @@
     @Test
     fun buttonShapes_mixedShapes_hasRoundedShapesIsFalse() {
         assertThat(
-                ButtonShapes(
+                ToggleButtonShapes(
                         shape = RectangleShape,
                         pressedShape = RoundedCornerShape(10.dp),
                         checkedShape = RoundedCornerShape(4.dp),
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.android.kt
index 12c544b..4c9bf48 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.android.kt
@@ -43,14 +43,23 @@
 internal actual fun rememberAccessibilityServiceState(
     listenToTouchExplorationState: Boolean,
     listenToSwitchAccessState: Boolean,
+    listenToVoiceAccessState: Boolean,
 ): State<Boolean> {
     val context = LocalContext.current
     val accessibilityManager =
         context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
 
     val listener =
-        remember(listenToTouchExplorationState, listenToSwitchAccessState) {
-            Listener(listenToTouchExplorationState, listenToSwitchAccessState)
+        remember(
+            listenToTouchExplorationState,
+            listenToSwitchAccessState,
+            listenToVoiceAccessState,
+        ) {
+            Listener(
+                listenToTouchExplorationState = listenToTouchExplorationState,
+                listenToSwitchAccessState = listenToSwitchAccessState,
+                listenToVoiceAccessState = listenToVoiceAccessState,
+            )
         }
 
     ObserveState(
@@ -85,7 +94,8 @@
 @Stable
 private class Listener(
     listenToTouchExplorationState: Boolean,
-    listenToSwitchAccessState: Boolean,
+    val listenToSwitchAccessState: Boolean,
+    val listenToVoiceAccessState: Boolean,
 ) : AccessibilityStateChangeListener, State<Boolean> {
     private var accessibilityEnabled by mutableStateOf(false)
 
@@ -102,13 +112,18 @@
             null
         }
 
-    private val switchAccessListener =
-        if (listenToSwitchAccessState && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+    private val otherA11yServicesListener =
+        if (
+            (listenToSwitchAccessState || listenToVoiceAccessState) &&
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+        ) {
             object : AccessibilityServicesStateChangeListener {
-                var enabled by mutableStateOf(false)
+                var switchAccessEnabled by mutableStateOf(false)
+                var voiceAccessEnabled by mutableStateOf(false)
 
                 override fun onAccessibilityServicesStateChanged(am: AccessibilityManager) {
-                    enabled = am.switchAccessEnabled
+                    switchAccessEnabled = am.switchAccessEnabled
+                    voiceAccessEnabled = am.voiceAccessEnabled
                 }
             }
         } else {
@@ -118,14 +133,25 @@
     private val AccessibilityManager.switchAccessEnabled
         get() =
             getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC).fastAny {
-                it.settingsActivityName?.contains(SwitchAccessActivityName) == true
+                it.settingsActivityName?.contains(SwitchAccessActivityName, ignoreCase = true) ==
+                    true
+            }
+
+    private val AccessibilityManager.voiceAccessEnabled
+        get() =
+            getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC).fastAny {
+                it.settingsActivityName?.contains(VoiceAccessActivityName, ignoreCase = true) ==
+                    true
             }
 
     override val value: Boolean
         get() =
             accessibilityEnabled &&
-                ((touchExplorationListener?.enabled ?: false) ||
-                    (switchAccessListener?.enabled ?: false))
+                ((touchExplorationListener?.enabled == true) ||
+                    (listenToSwitchAccessState &&
+                        otherA11yServicesListener?.switchAccessEnabled == true) ||
+                    (listenToVoiceAccessState &&
+                        otherA11yServicesListener?.voiceAccessEnabled == true))
 
     override fun onAccessibilityStateChanged(enabled: Boolean) {
         accessibilityEnabled = enabled
@@ -139,8 +165,9 @@
             am.addTouchExplorationStateChangeListener(it)
         }
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-            switchAccessListener?.let {
-                it.enabled = am.switchAccessEnabled
+            otherA11yServicesListener?.let {
+                it.switchAccessEnabled = am.switchAccessEnabled
+                it.voiceAccessEnabled = am.voiceAccessEnabled
                 Api33Impl.addAccessibilityServicesStateChangeListener(am, it)
             }
         }
@@ -150,7 +177,7 @@
         am.removeAccessibilityStateChangeListener(this)
         touchExplorationListener?.let { am.removeTouchExplorationStateChangeListener(it) }
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-            switchAccessListener?.let {
+            otherA11yServicesListener?.let {
                 Api33Impl.removeAccessibilityServicesStateChangeListener(am, it)
             }
         }
@@ -177,3 +204,4 @@
 }
 
 private const val SwitchAccessActivityName = "SwitchAccess"
+private const val VoiceAccessActivityName = "VoiceAccess"
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index 610a237..04de614 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -1050,7 +1050,7 @@
  * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external"
  * target="_blank">Material Design bottom app bar</a>.
  *
- * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ * A bottom app bar displays navigation and key actions at the bottom of small screens.
  *
  * ![Bottom app bar
  * image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
@@ -1106,7 +1106,7 @@
  * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external"
  * target="_blank">Material Design bottom app bar</a>.
  *
- * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ * A bottom app bar displays navigation and key actions at the bottom of small screens.
  *
  * ![Bottom app bar
  * image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
@@ -1186,7 +1186,7 @@
  * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external"
  * target="_blank">Material Design bottom app bar</a>.
  *
- * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ * A bottom app bar displays navigation and key actions at the bottom of small screens.
  *
  * ![Bottom app bar
  * image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
@@ -1235,7 +1235,7 @@
  * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external"
  * target="_blank">Material Design bottom app bar</a>.
  *
- * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ * A bottom app bar displays navigation and key actions at the bottom of small screens.
  *
  * ![Bottom app bar
  * image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
@@ -1288,14 +1288,16 @@
     )
 }
 
+// TODO missing image of the flexible bottom app bar.
 /**
  * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external"
- * target="_blank">Material Design bottom app bar</a>.
+ * target="_blank">Material Design flexible bottom app bar</a>.
  *
- * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ * A flexible bottom app bar displays navigation and key actions at the bottom of small screens.
  *
- * ![Bottom app bar
- * image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ * This variation of the Bottom app bar has a [horizontalArrangement] parameter for controlling the
+ * way the content is arranged. Also, it allows more flexibility in controlling the bar's expanded
+ * height with an [expandedHeight] value.
  *
  * If you are interested in displaying a [FloatingActionButton], consider using another overload
  * that takes a [FloatingActionButton] parameter.
@@ -1311,7 +1313,6 @@
  * @sample androidx.compose.material3.samples.ExitAlwaysBottomAppBarSpacedEvenly
  * @sample androidx.compose.material3.samples.ExitAlwaysBottomAppBarFixed
  * @sample androidx.compose.material3.samples.ExitAlwaysBottomAppBarFixedVibrant
- * @param horizontalArrangement the horizontal arrangement of the content.
  * @param modifier the [Modifier] to be applied to this BottomAppBar
  * @param containerColor the color used for the background of this BottomAppBar. Use
  *   [Color.Transparent] to have no color.
@@ -1319,6 +1320,13 @@
  *   the matching content color for [containerColor], or to the current [LocalContentColor] if
  *   [containerColor] is not a color from the theme.
  * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param horizontalArrangement the horizontal arrangement of the content inside this BottomAppBar
+ * @param expandedHeight the maximum height this bottom bar can reach when fully expanded. If a
+ *   [scrollBehavior] is provided, the bar might collapse or expand based on scrolling. In that
+ *   case, this value sets the upper limit for the bar's height during expansion. This [Dp] value
+ *   must be specified, finite, and greater than zero; otherwise,
+ *   [BottomAppBarDefaults.FlexibleBottomAppBarHeight] will be used as a default. In case the
+ *   [scrollBehavior] is `null`, this value will simply be the fixed height of the bottom bar.
  * @param windowInsets a window insets that app bar will respect.
  * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
  *   be applied by this bottom app bar to set up its height. A scroll behavior is designed to work
@@ -1330,23 +1338,30 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @ExperimentalMaterial3ExpressiveApi
 @Composable
-fun BottomAppBar(
-    horizontalArrangement: Arrangement.Horizontal,
+fun FlexibleBottomAppBar(
     modifier: Modifier = Modifier,
     containerColor: Color = BottomAppBarDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
-    contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp), // TODO tokens
+    contentPadding: PaddingValues = BottomAppBarDefaults.FlexibleContentPadding,
+    horizontalArrangement: Arrangement.Horizontal =
+        BottomAppBarDefaults.FlexibleHorizontalArrangement,
+    expandedHeight: Dp = BottomAppBarDefaults.FlexibleBottomAppBarHeight,
     windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
     scrollBehavior: BottomAppBarScrollBehavior? = null,
     content: @Composable RowScope.() -> Unit
 ) {
     BottomAppBarLayout(
-        containerHeight = 64.dp, // TODO tokens
+        containerHeight =
+            if (expandedHeight.isFinite && expandedHeight.isSpecified && expandedHeight > 0.dp) {
+                expandedHeight
+            } else {
+                BottomAppBarDefaults.FlexibleBottomAppBarHeight
+            },
         horizontalArrangement = horizontalArrangement,
         modifier = modifier,
         containerColor = containerColor,
         contentColor = contentColor,
-        tonalElevation = BottomAppBarDefaults.ContainerElevation,
+        tonalElevation = AppBarTokens.ContainerElevation,
         contentPadding = contentPadding,
         windowInsets = windowInsets,
         scrollBehavior = scrollBehavior,
@@ -2192,7 +2207,34 @@
     val bottomAppBarFabColor: Color
         @Composable get() = FabSecondaryContainerTokens.ContainerColor.value
 
-    val HorizontalArrangement =
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ExperimentalMaterial3ExpressiveApi
+    @ExperimentalMaterial3ExpressiveApi
+    /** Default padding used for [FlexibleBottomAppBar]. */
+    val FlexibleContentPadding = PaddingValues(horizontal = 16.dp) // TODO tokens
+
+    /**
+     * Default height of a flexible [FlexibleBottomAppBar]. The height here represents the height of
+     * the bottom app bar in its expanded state.
+     */
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ExperimentalMaterial3ExpressiveApi
+    @ExperimentalMaterial3ExpressiveApi
+    val FlexibleBottomAppBarHeight = AppBarSmallTokens.ContainerHeight
+
+    /** A default [Arrangement] that will be used to space a [FlexibleBottomAppBar]'s content. */
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ExperimentalMaterial3ExpressiveApi
+    @ExperimentalMaterial3ExpressiveApi
+    val FlexibleHorizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween
+
+    /**
+     * An [Arrangement] that will be used to space [FlexibleBottomAppBar]'s with a fixed spacing.
+     */
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ExperimentalMaterial3ExpressiveApi
+    @ExperimentalMaterial3ExpressiveApi
+    val FlexibleFixedHorizontalArrangement: Arrangement.Horizontal =
         Arrangement.spacedBy(32.dp, Alignment.CenterHorizontally) // TODO tokens
 
     // TODO: note that this scroll behavior may impact assistive technologies making the component
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index 6600a99..f24ae75 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material3
 
 import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.FiniteAnimationSpec
 import androidx.compose.animation.core.VectorConverter
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.interaction.FocusInteraction
@@ -25,14 +26,20 @@
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.interaction.collectIsPressedAsState
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
 import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.internal.ProvideContentColorTextStyle
 import androidx.compose.material3.internal.animateElevation
+import androidx.compose.material3.internal.rememberAnimatedShape
 import androidx.compose.material3.tokens.BaselineButtonTokens
 import androidx.compose.material3.tokens.ButtonLargeTokens
 import androidx.compose.material3.tokens.ButtonMediumTokens
@@ -43,6 +50,7 @@
 import androidx.compose.material3.tokens.ElevatedButtonTokens
 import androidx.compose.material3.tokens.FilledButtonTokens
 import androidx.compose.material3.tokens.FilledTonalButtonTokens
+import androidx.compose.material3.tokens.MotionSchemeKeyTokens
 import androidx.compose.material3.tokens.OutlinedButtonTokens
 import androidx.compose.material3.tokens.TextButtonTokens
 import androidx.compose.runtime.Composable
@@ -50,6 +58,8 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -58,6 +68,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
@@ -174,6 +185,137 @@
     }
 }
 
+// TODO add link to image of pressed button
+/**
+ * <a href="https://m3.material.io/components/buttons/overview" class="external"
+ * target="_blank">Material Design button</a>.
+ *
+ * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a
+ * post. It also morphs between the shapes provided in [shapes] depending on the state of the
+ * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in
+ * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according
+ * to user interaction.
+ *
+ * ![Filled button
+ * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-button.png)
+ *
+ * Filled buttons are high-emphasis buttons. Filled buttons have the most visual impact after the
+ * [FloatingActionButton], and should be used for important, final actions that complete a flow,
+ * like "Save", "Join now", or "Confirm".
+ *
+ * @sample androidx.compose.material3.samples.ButtonWithAnimatedShapeSample
+ *
+ * Choose the best button for an action based on the amount of emphasis it needs. The more important
+ * an action is, the higher emphasis its button should be.
+ * - See [OutlinedButton] for a medium-emphasis button with a border.
+ * - See [ElevatedButton] for an [FilledTonalButton] with a shadow.
+ * - See [TextButton] for a low-emphasis button with no border.
+ * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button].
+ *
+ * The default text style for internal [Text] components will be set to [Typography.labelLarge].
+ *
+ * @param onClick called when this button is clicked
+ * @param shapes the [ButtonShapes] that this button with morph between depending on the user's
+ *   interaction with the button.
+ * @param modifier the [Modifier] to be applied to this button
+ * @param enabled controls the enabled state of this button. When `false`, this component will not
+ *   respond to user input, and it will appear visually disabled and disabled to accessibility
+ *   services.
+ * @param colors [ButtonColors] that will be used to resolve the colors for this button in different
+ *   states. See [ButtonDefaults.buttonColors].
+ * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
+ *   states. This controls the size of the shadow below the button. See
+ *   [ButtonElevation.shadowElevation].
+ * @param border the border to draw around the container of this button
+ * @param contentPadding the spacing values to apply internally between the container and the
+ *   content
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ *   emitting [Interaction]s for this button. You can use this to change the button's appearance or
+ *   preview the button in different states. Note that if `null` is provided, interactions will
+ *   still happen internally.
+ * @param content The content displayed on the button, expected to be text, icon or image.
+ */
+@Composable
+@ExperimentalMaterial3ExpressiveApi
+fun Button(
+    onClick: () -> Unit,
+    shapes: ButtonShapes,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
+    elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
+    border: BorderStroke? = null,
+    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
+    interactionSource: MutableInteractionSource? = null,
+    content: @Composable RowScope.() -> Unit
+) {
+    @Suppress("NAME_SHADOWING")
+    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
+    // TODO Load the motionScheme tokens from the component tokens file
+    // MotionSchemeKeyTokens.DefaultEffects is intentional here to prevent
+    // any bounce in this component.
+    val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
+    val pressed by interactionSource.collectIsPressedAsState()
+    val containerColor = colors.containerColor(enabled)
+    val contentColor = colors.contentColor(enabled)
+    val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
+    val layoutDirection = LocalLayoutDirection.current
+    val buttonShape = shapeByInteraction(shapes, pressed, defaultAnimationSpec)
+
+    Surface(
+        onClick = onClick,
+        modifier = modifier.semantics { role = Role.Button },
+        enabled = enabled,
+        shape = buttonShape,
+        color = containerColor,
+        contentColor = contentColor,
+        shadowElevation = shadowElevation,
+        border = border,
+        interactionSource = interactionSource
+    ) {
+        ProvideContentColorTextStyle(
+            contentColor = contentColor,
+            textStyle = MaterialTheme.typography.labelLarge
+        ) {
+            Row(
+                Modifier.defaultMinSize(
+                        minWidth = ButtonDefaults.MinWidth,
+                        minHeight = ButtonDefaults.MinHeight
+                    )
+                    .then(
+                        when (buttonShape) {
+                            is ShapeWithHorizontalCenterOptically -> {
+                                Modifier.horizontalCenterOptically(
+                                    shape = buttonShape,
+                                    maxStartOffset =
+                                        contentPadding.calculateStartPadding(layoutDirection),
+                                    maxEndOffset =
+                                        contentPadding.calculateEndPadding(layoutDirection)
+                                )
+                            }
+                            is CornerBasedShape -> {
+                                Modifier.horizontalCenterOptically(
+                                    shape = buttonShape,
+                                    maxStartOffset =
+                                        contentPadding.calculateStartPadding(layoutDirection),
+                                    maxEndOffset =
+                                        contentPadding.calculateEndPadding(layoutDirection)
+                                )
+                            }
+                            else -> {
+                                Modifier
+                            }
+                        }
+                    )
+                    .padding(contentPadding),
+                horizontalArrangement = Arrangement.Center,
+                verticalAlignment = Alignment.CenterVertically,
+                content = content
+            )
+        }
+    }
+}
+
 /**
  * <a href="https://m3.material.io/components/buttons/overview" class="external"
  * target="_blank">Material Design elevated button</a>.
@@ -247,6 +389,84 @@
         content = content
     )
 
+// TODO add link to image of pressed elevated button
+/**
+ * <a href="https://m3.material.io/components/buttons/overview" class="external"
+ * target="_blank">Material Design elevated button</a>.
+ *
+ * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a
+ * post. It also morphs between the shapes provided in [shapes] depending on the state of the
+ * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in
+ * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according
+ * to user interaction.
+ *
+ * ![Elevated button
+ * image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-button.png)
+ *
+ * Elevated buttons are high-emphasis buttons that are essentially [FilledTonalButton]s with a
+ * shadow. To prevent shadow creep, only use them when absolutely necessary, such as when the button
+ * requires visual separation from patterned container.
+ *
+ * @sample androidx.compose.material3.samples.ElevatedButtonWithAnimatedShapeSample
+ *
+ * Choose the best button for an action based on the amount of emphasis it needs. The more important
+ * an action is, the higher emphasis its button should be.
+ * - See [Button] for a high-emphasis button without a shadow, also known as a filled button.
+ * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button].
+ * - See [OutlinedButton] for a medium-emphasis button with a border.
+ * - See [TextButton] for a low-emphasis button with no border.
+ *
+ * The default text style for internal [Text] components will be set to [Typography.labelLarge].
+ *
+ * @param onClick called when this button is clicked
+ * @param shapes the [ButtonShapes] that this button with morph between depending on the user's
+ *   interaction with the button.
+ * @param modifier the [Modifier] to be applied to this button
+ * @param enabled controls the enabled state of this button. When `false`, this component will not
+ *   respond to user input, and it will appear visually disabled and disabled to accessibility
+ *   services.
+ * @param colors [ButtonColors] that will be used to resolve the colors for this button in different
+ *   states. See [ButtonDefaults.elevatedButtonColors].
+ * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
+ *   states. This controls the size of the shadow below the button. Additionally, when the container
+ *   color is [ColorScheme.surface], this controls the amount of primary color applied as an
+ *   overlay. See [ButtonDefaults.elevatedButtonElevation].
+ * @param border the border to draw around the container of this button
+ * @param contentPadding the spacing values to apply internally between the container and the
+ *   content
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ *   emitting [Interaction]s for this button. You can use this to change the button's appearance or
+ *   preview the button in different states. Note that if `null` is provided, interactions will
+ *   still happen internally.
+ * @param content The content displayed on the button, expected to be text, icon or image.
+ */
+@Composable
+@ExperimentalMaterial3ExpressiveApi
+fun ElevatedButton(
+    onClick: () -> Unit,
+    shapes: ButtonShapes,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    colors: ButtonColors = ButtonDefaults.elevatedButtonColors(),
+    elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
+    border: BorderStroke? = null,
+    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
+    interactionSource: MutableInteractionSource? = null,
+    content: @Composable RowScope.() -> Unit
+) =
+    Button(
+        onClick = onClick,
+        shapes = shapes,
+        modifier = modifier,
+        enabled = enabled,
+        colors = colors,
+        elevation = elevation,
+        border = border,
+        contentPadding = contentPadding,
+        interactionSource = interactionSource,
+        content = content
+    )
+
 /**
  * <a href="https://m3.material.io/components/buttons/overview" class="external"
  * target="_blank">Material Design filled tonal button</a>.
@@ -321,6 +541,85 @@
         content = content
     )
 
+// TODO add link to image of pressed filled tonal button
+/**
+ * <a href="https://m3.material.io/components/buttons/overview" class="external"
+ * target="_blank">Material Design filled tonal button</a>.
+ *
+ * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a
+ * post. It also morphs between the shapes provided in [shapes] depending on the state of the
+ * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in
+ * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according
+ * to user interaction.
+ *
+ * ![Filled tonal button
+ * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-tonal-button.png)
+ *
+ * Filled tonal buttons are medium-emphasis buttons that is an alternative middle ground between
+ * default [Button]s (filled) and [OutlinedButton]s. They can be used in contexts where
+ * lower-priority button requires slightly more emphasis than an outline would give, such as "Next"
+ * in an onboarding flow. Tonal buttons use the secondary color mapping.
+ *
+ * @sample androidx.compose.material3.samples.FilledTonalButtonWithAnimatedShapeSample
+ *
+ * Choose the best button for an action based on the amount of emphasis it needs. The more important
+ * an action is, the higher emphasis its button should be.
+ * - See [Button] for a high-emphasis button without a shadow, also known as a filled button.
+ * - See [ElevatedButton] for a [FilledTonalButton] with a shadow.
+ * - See [OutlinedButton] for a medium-emphasis button with a border.
+ * - See [TextButton] for a low-emphasis button with no border.
+ *
+ * The default text style for internal [Text] components will be set to [Typography.labelLarge].
+ *
+ * @param onClick called when this button is clicked
+ * @param shapes the [ButtonShapes] that this button with morph between depending on the user's
+ *   interaction with the button.
+ * @param modifier the [Modifier] to be applied to this button
+ * @param enabled controls the enabled state of this button. When `false`, this component will not
+ *   respond to user input, and it will appear visually disabled and disabled to accessibility
+ *   services.
+ * @param colors [ButtonColors] that will be used to resolve the colors for this button in different
+ *   states. See [ButtonDefaults.filledTonalButtonColors].
+ * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
+ *   states. This controls the size of the shadow below the button. Additionally, when the container
+ *   color is [ColorScheme.surface], this controls the amount of primary color applied as an
+ *   overlay.
+ * @param border the border to draw around the container of this button
+ * @param contentPadding the spacing values to apply internally between the container and the
+ *   content
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ *   emitting [Interaction]s for this button. You can use this to change the button's appearance or
+ *   preview the button in different states. Note that if `null` is provided, interactions will
+ *   still happen internally.
+ * @param content The content displayed on the button, expected to be text, icon or image.
+ */
+@Composable
+@ExperimentalMaterial3ExpressiveApi
+fun FilledTonalButton(
+    onClick: () -> Unit,
+    shapes: ButtonShapes,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
+    elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(),
+    border: BorderStroke? = null,
+    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
+    interactionSource: MutableInteractionSource? = null,
+    content: @Composable RowScope.() -> Unit
+) =
+    Button(
+        onClick = onClick,
+        shapes = shapes,
+        modifier = modifier,
+        enabled = enabled,
+        colors = colors,
+        elevation = elevation,
+        border = border,
+        contentPadding = contentPadding,
+        interactionSource = interactionSource,
+        content = content
+    )
+
 /**
  * <a href="https://m3.material.io/components/buttons/overview" class="external"
  * target="_blank">Material Design outlined button</a>.
@@ -394,6 +693,84 @@
         content = content
     )
 
+// TODO add link to image of pressed outlined button
+/**
+ * <a href="https://m3.material.io/components/buttons/overview" class="external"
+ * target="_blank">Material Design outlined button</a>.
+ *
+ * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a
+ * post. It also morphs between the shapes provided in [shapes] depending on the state of the
+ * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in
+ * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according
+ * to user interaction.
+ *
+ * ![Outlined button
+ * image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-button.png)
+ *
+ * Outlined buttons are medium-emphasis buttons. They contain actions that are important, but are
+ * not the primary action in an app. Outlined buttons pair well with [Button]s to indicate an
+ * alternative, secondary action.
+ *
+ * @sample androidx.compose.material3.samples.OutlinedButtonWithAnimatedShapeSample
+ *
+ * Choose the best button for an action based on the amount of emphasis it needs. The more important
+ * an action is, the higher emphasis its button should be.
+ * - See [Button] for a high-emphasis button without a shadow, also known as a filled button.
+ * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button].
+ * - See [OutlinedButton] for a medium-emphasis button with a border.
+ * - See [TextButton] for a low-emphasis button with no border.
+ *
+ * The default text style for internal [Text] components will be set to [Typography.labelLarge].
+ *
+ * @param onClick called when this button is clicked
+ * @param shapes the [ButtonShapes] that this button with morph between depending on the user's
+ *   interaction with the button.
+ * @param modifier the [Modifier] to be applied to this button
+ * @param enabled controls the enabled state of this button. When `false`, this component will not
+ *   respond to user input, and it will appear visually disabled and disabled to accessibility
+ *   services.
+ * @param colors [ButtonColors] that will be used to resolve the colors for this button in different
+ *   states. See [ButtonDefaults.outlinedButtonColors].
+ * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
+ *   states. This controls the size of the shadow below the button. Additionally, when the container
+ *   color is [ColorScheme.surface], this controls the amount of primary color applied as an
+ *   overlay.
+ * @param border the border to draw around the container of this button. Pass `null` for no border.
+ * @param contentPadding the spacing values to apply internally between the container and the
+ *   content
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ *   emitting [Interaction]s for this button. You can use this to change the button's appearance or
+ *   preview the button in different states. Note that if `null` is provided, interactions will
+ *   still happen internally.
+ * @param content The content displayed on the button, expected to be text, icon or image.
+ */
+@Composable
+@ExperimentalMaterial3ExpressiveApi
+fun OutlinedButton(
+    onClick: () -> Unit,
+    shapes: ButtonShapes,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
+    elevation: ButtonElevation? = null,
+    border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled),
+    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
+    interactionSource: MutableInteractionSource? = null,
+    content: @Composable RowScope.() -> Unit
+) =
+    Button(
+        onClick = onClick,
+        shapes = shapes,
+        modifier = modifier,
+        enabled = enabled,
+        colors = colors,
+        elevation = elevation,
+        border = border,
+        contentPadding = contentPadding,
+        interactionSource = interactionSource,
+        content = content
+    )
+
 /**
  * <a href="https://m3.material.io/components/buttons/overview" class="external"
  * target="_blank">Material Design text button</a>.
@@ -408,7 +785,7 @@
  * and cards. In cards, text buttons help maintain an emphasis on card content. Text buttons are
  * used for the lowest priority actions, especially when presenting multiple options.
  *
- * @sample androidx.compose.material3.samples.TextButtonSample
+ * @sample androidx.compose.material3.samples.TextButtonWithAnimatedShapeSample
  *
  * Choose the best button for an action based on the amount of emphasis it needs. The more important
  * an action is, the higher emphasis its button should be.
@@ -468,6 +845,85 @@
         content = content
     )
 
+// TODO add link to image of pressed text button
+/**
+ * <a href="https://m3.material.io/components/buttons/overview" class="external"
+ * target="_blank">Material Design text button</a>.
+ *
+ * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a
+ * post. It also morphs between the shapes provided in [shapes] depending on the state of the
+ * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in
+ * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according
+ * to user interaction.
+ *
+ * ![Text button
+ * image](https://developer.android.com/images/reference/androidx/compose/material3/text-button.png)
+ *
+ * Text buttons are typically used for less-pronounced actions, including those located in dialogs
+ * and cards. In cards, text buttons help maintain an emphasis on card content. Text buttons are
+ * used for the lowest priority actions, especially when presenting multiple options.
+ *
+ * @sample androidx.compose.material3.samples.TextButtonSample
+ *
+ * Choose the best button for an action based on the amount of emphasis it needs. The more important
+ * an action is, the higher emphasis its button should be.
+ * - See [Button] for a high-emphasis button without a shadow, also known as a filled button.
+ * - See [ElevatedButton] for a [FilledTonalButton] with a shadow.
+ * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button].
+ * - See [OutlinedButton] for a medium-emphasis button with a border.
+ *
+ * The default text style for internal [Text] components will be set to [Typography.labelLarge].
+ *
+ * @param onClick called when this button is clicked
+ * @param shapes the [ButtonShapes] that this button with morph between depending on the user's
+ *   interaction with the button.
+ * @param modifier the [Modifier] to be applied to this button
+ * @param enabled controls the enabled state of this button. When `false`, this component will not
+ *   respond to user input, and it will appear visually disabled and disabled to accessibility
+ *   services.
+ * @param colors [ButtonColors] that will be used to resolve the colors for this button in different
+ *   states. See [ButtonDefaults.textButtonColors].
+ * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
+ *   states. This controls the size of the shadow below the button. Additionally, when the container
+ *   color is [ColorScheme.surface], this controls the amount of primary color applied as an
+ *   overlay. A TextButton typically has no elevation, and the default value is `null`. See
+ *   [ElevatedButton] for a button with elevation.
+ * @param border the border to draw around the container of this button
+ * @param contentPadding the spacing values to apply internally between the container and the
+ *   content
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ *   emitting [Interaction]s for this button. You can use this to change the button's appearance or
+ *   preview the button in different states. Note that if `null` is provided, interactions will
+ *   still happen internally.
+ * @param content The content displayed on the button, expected to be text.
+ */
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun TextButton(
+    onClick: () -> Unit,
+    shapes: ButtonShapes,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    colors: ButtonColors = ButtonDefaults.textButtonColors(),
+    elevation: ButtonElevation? = null,
+    border: BorderStroke? = null,
+    contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
+    interactionSource: MutableInteractionSource? = null,
+    content: @Composable RowScope.() -> Unit
+) =
+    Button(
+        onClick = onClick,
+        shapes = shapes,
+        modifier = modifier,
+        enabled = enabled,
+        colors = colors,
+        elevation = elevation,
+        border = border,
+        contentPadding = contentPadding,
+        interactionSource = interactionSource,
+        content = content
+    )
+
 // TODO(b/201341237): Use token values for 0 elevation?
 // TODO(b/201341237): Use token values for null border?
 // TODO(b/201341237): Use token values for no color (transparent)?
@@ -702,13 +1158,20 @@
     @ExperimentalMaterial3ExpressiveApi
     val XLargeIconSpacing = ButtonXLargeTokens.IconLabelSpace
 
-    /** Square shape for any button. */
+    /** Square shape for default buttons. */
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
     @get:ExperimentalMaterial3ExpressiveApi
     @ExperimentalMaterial3ExpressiveApi
     val squareShape: Shape
         @Composable get() = ButtonSmallTokens.ContainerShapeSquare.value
 
+    /** Pressed shape for default buttons. */
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ExperimentalMaterial3ExpressiveApi
+    @ExperimentalMaterial3ExpressiveApi
+    val pressedShape: Shape
+        @Composable get() = ButtonSmallTokens.PressedContainerShape.value
+
     /** Default shape for a button. */
     val shape: Shape
         @Composable get() = ButtonSmallTokens.ContainerShapeRound.value
@@ -730,6 +1193,24 @@
         @Composable get() = ButtonSmallTokens.ContainerShapeRound.value
 
     /**
+     * Creates a [ButtonShapes] that represents the default shape and pressed shape used in a
+     * button.
+     */
+    @ExperimentalMaterial3ExpressiveApi
+    @Composable
+    fun shapes() = MaterialTheme.shapes.defaultButtonShapes
+
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    internal val Shapes.defaultButtonShapes: ButtonShapes
+        @Composable
+        get() {
+            return defaultButtonShapesCached
+                ?: ButtonShapes(shape = shape, pressedShape = pressedShape).also {
+                    defaultButtonShapesCached = it
+                }
+        }
+
+    /**
      * Creates a [ButtonColors] that represents the default container and content colors used in a
      * [Button].
      */
@@ -1277,3 +1758,72 @@
         return result
     }
 }
+
+/**
+ * The shapes that will be used in buttons. Button will morph between these shapes depending on the
+ * interaction of the button, assuming all of the shapes are [CornerBasedShape]s.
+ *
+ * @property shape is the active shape.
+ * @property pressedShape is the pressed shape.
+ */
+@ExperimentalMaterial3ExpressiveApi
+@Immutable
+class ButtonShapes(val shape: Shape, val pressedShape: Shape) {
+    /** Returns a copy of this ButtonShapes, optionally overriding some of the values. */
+    fun copy(
+        shape: Shape? = this.shape,
+        pressedShape: Shape? = this.pressedShape,
+    ) =
+        ButtonShapes(
+            shape = shape.takeOrElse { this.shape },
+            pressedShape = pressedShape.takeOrElse { this.pressedShape }
+        )
+
+    internal fun Shape?.takeOrElse(block: () -> Shape): Shape = this ?: block()
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || other !is ButtonShapes) return false
+
+        if (shape != other.shape) return false
+        if (pressedShape != other.pressedShape) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = shape.hashCode()
+        result = 31 * result + pressedShape.hashCode()
+
+        return result
+    }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+internal val ButtonShapes.hasRoundedCornerShapes: Boolean
+    get() = shape is RoundedCornerShape && pressedShape is RoundedCornerShape
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun shapeByInteraction(
+    shapes: ButtonShapes,
+    pressed: Boolean,
+    animationSpec: FiniteAnimationSpec<Float>
+): Shape {
+    val shape =
+        if (pressed) {
+            shapes.pressedShape
+        } else {
+            shapes.shape
+        }
+
+    if (shapes.hasRoundedCornerShapes)
+        return key(shapes) {
+            rememberAnimatedShape(
+                shape as RoundedCornerShape,
+                animationSpec,
+            )
+        }
+
+    return shape
+}
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/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
index 860584f..a620105 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
@@ -55,6 +55,7 @@
 import androidx.compose.foundation.layout.exclude
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.imePadding
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
 import androidx.compose.foundation.layout.only
@@ -139,14 +140,13 @@
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.layout.positionOnScreen
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.InterceptPlatformTextInput
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 import androidx.compose.ui.platform.SoftwareKeyboardController
 import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.TextStyle
@@ -154,18 +154,28 @@
 import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntRect
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.constrain
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.lerp
+import androidx.compose.ui.unit.offset
 import androidx.compose.ui.unit.round
 import androidx.compose.ui.util.fastFirst
 import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.util.fastMaxOfOrNull
 import androidx.compose.ui.util.lerp
 import androidx.compose.ui.window.DialogProperties
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
 import androidx.compose.ui.zIndex
 import kotlin.coroutines.cancellation.CancellationException
 import kotlin.math.abs
@@ -188,7 +198,8 @@
  * image](https://developer.android.com/images/reference/androidx/compose/material3/search-bar.png)
  *
  * The [SearchBar] component represents a search bar in the collapsed state. It should be used in
- * conjunction with an [ExpandedFullScreenSearchBar] to display search results when expanded.
+ * conjunction with an [ExpandedFullScreenSearchBar] or [ExpandedDockedSearchBar] to display search
+ * results when expanded.
  *
  * @param state the state of the search bar. This state should also be passed to the [inputField]
  *   and the expanded search bar.
@@ -243,8 +254,8 @@
  * A [TopSearchBar] is a [SearchBar] with additional handling for top app bar behavior, such as
  * window insets and scrolling. Using a [TopSearchBar] as the top bar of a [Scaffold] ensures that
  * the search bar remains at the top of the screen. Like with [SearchBar], [TopSearchBar] should be
- * used in conjunction with an [ExpandedFullScreenSearchBar] to display search results when
- * expanded.
+ * used in conjunction with an [ExpandedFullScreenSearchBar] or [ExpandedDockedSearchBar] to display
+ * search results when expanded.
  *
  * @param state the state of the search bar. This state should also be passed to the [inputField]
  *   and the expanded search bar.
@@ -301,6 +312,8 @@
 /**
  * [ExpandedFullScreenSearchBar] represents a search bar that is currently expanding or in the
  * expanded state, showing search results. This component is displayed in a new full-screen dialog.
+ * If this expansion behavior is undesirable, for example on medium or large screens such as
+ * tablets, [ExpandedDockedSearchBar] can be used instead.
  *
  * @param state the state of the search bar. This state should also be passed to the [inputField]
  *   and the collapsed search bar.
@@ -369,7 +382,91 @@
 
         // Focus the input field on the first expansion,
         // but no need to re-focus if the focus gets cleared.
-        LaunchedEffect(state.isExpanded) { focusRequester.requestFocus() }
+        LaunchedEffect(Unit) { focusRequester.requestFocus() }
+    }
+}
+
+/**
+ * [ExpandedDockedSearchBar] represents a search bar that is currently expanding or in the expanded
+ * state, showing search results. This component is displayed in a popup over the collapsed search
+ * bar. It is recommended to use [ExpandedDockedSearchBar] on medium and large screens such as
+ * tablets, and to instead use [ExpandedFullScreenSearchBar] on compact screen such as phones.
+ *
+ * @param state the state of the search bar. This state should also be passed to the [inputField]
+ *   and the collapsed search bar.
+ * @param inputField the input field of this search bar that allows entering a query, typically a
+ *   [SearchBarDefaults.InputField].
+ * @param modifier the [Modifier] to be applied to this expanded search bar.
+ * @param shape the shape of this search bar.
+ * @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
+ *   in different states. See [SearchBarDefaults.colors].
+ * @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
+ *   translucent primary color overlay is applied on top of the container. A higher tonal elevation
+ *   value will result in a darker color in light theme and lighter color in dark theme. See also:
+ *   [Surface].
+ * @param shadowElevation the elevation for the shadow below this search bar.
+ * @param properties the platform-specific properties to configure the dialog's behavior. Any
+ *   properties which limit the dialog's size (e.g. [DialogProperties.usePlatformDefaultWidth]) are
+ *   ignored.
+ * @param content the content of this search bar to display search results below the [inputField].
+ */
+@ExperimentalMaterial3Api
+@Composable
+internal fun ExpandedDockedSearchBar(
+    state: SearchBarState,
+    inputField: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    shape: Shape = SearchBarDefaults.dockedShape,
+    colors: SearchBarColors = SearchBarDefaults.colors(),
+    tonalElevation: Dp = SearchBarDefaults.TonalElevation,
+    shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
+    properties: PopupProperties = PopupProperties(focusable = true, clippingEnabled = false),
+    content: @Composable ColumnScope.() -> Unit,
+) {
+    if (!state.isExpanded) return
+
+    val positionProvider =
+        remember(state) {
+            object : PopupPositionProvider {
+                override fun calculatePosition(
+                    anchorBounds: IntRect,
+                    windowSize: IntSize,
+                    layoutDirection: LayoutDirection,
+                    popupContentSize: IntSize
+                ): IntOffset = state.collapsedBounds.topLeft
+            }
+        }
+
+    val scope = rememberCoroutineScope()
+
+    Popup(
+        popupPositionProvider = positionProvider,
+        onDismissRequest = { scope.launch { state.animateToCollapsed() } },
+        properties = properties,
+    ) {
+        val focusRequester = remember { FocusRequester() }
+
+        DockedSearchBarLayout(
+            state = state,
+            inputField = {
+                Box(
+                    modifier = Modifier.focusRequester(focusRequester),
+                    propagateMinConstraints = true,
+                ) {
+                    inputField()
+                }
+            },
+            modifier = modifier,
+            shape = shape,
+            colors = colors,
+            tonalElevation = tonalElevation,
+            shadowElevation = shadowElevation,
+            content = content,
+        )
+
+        // Focus the input field on the first expansion,
+        // but no need to re-focus if the focus gets cleared.
+        LaunchedEffect(Unit) { focusRequester.requestFocus() }
     }
 }
 
@@ -1201,10 +1298,6 @@
                         if (searchBarState.isExpanded) {
                             stateDescription = suggestionsAvailableSemantics
                         }
-                        onClick {
-                            focusRequester.requestFocus()
-                            true
-                        }
                     },
             enabled = enabled,
             readOnly = readOnly,
@@ -1360,10 +1453,6 @@
                         if (expanded) {
                             stateDescription = suggestionsAvailableSemantics
                         }
-                        onClick {
-                            focusRequester.requestFocus()
-                            true
-                        }
                     },
             enabled = enabled,
             readOnly = readOnly,
@@ -1499,10 +1588,6 @@
                         if (expanded) {
                             stateDescription = suggestionsAvailableSemantics
                         }
-                        onClick {
-                            focusRequester.requestFocus()
-                            true
-                        }
                     },
             enabled = enabled,
             singleLine = true,
@@ -2125,6 +2210,83 @@
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
+private fun DockedSearchBarLayout(
+    state: SearchBarState,
+    inputField: @Composable () -> Unit,
+    modifier: Modifier,
+    shape: Shape,
+    colors: SearchBarColors,
+    tonalElevation: Dp,
+    shadowElevation: Dp,
+    content: @Composable ColumnScope.() -> Unit,
+) {
+    val scope = rememberCoroutineScope()
+    BackHandler(enabled = state.isExpanded) { scope.launch { state.animateToCollapsed() } }
+
+    Surface(
+        shape = shape,
+        color = colors.containerColor,
+        contentColor = contentColorFor(colors.containerColor),
+        tonalElevation = tonalElevation,
+        shadowElevation = shadowElevation,
+        modifier = modifier.imePadding(),
+    ) {
+        val windowContainerHeight = getWindowContainerHeight()
+        val maxHeight = windowContainerHeight * DockedExpandedTableMaxHeightScreenRatio
+        val minHeight = DockedExpandedTableMinHeight.coerceAtMost(maxHeight)
+
+        Layout(
+            contents =
+                listOf(
+                    inputField,
+                    {
+                        Column {
+                            HorizontalDivider(color = colors.dividerColor)
+                            content()
+                        }
+                    },
+                )
+        ) { measurables, baseConstraints ->
+            val (inputFieldMeasurables, contentMeasurables) = measurables
+            val constraintMaxHeight =
+                lerp(state.collapsedBounds.height, maxHeight.roundToPx(), state.progress)
+            val constraints =
+                baseConstraints.constrain(
+                    Constraints(
+                        minHeight = minHeight.roundToPx().coerceAtMost(constraintMaxHeight),
+                        maxHeight = constraintMaxHeight,
+                    )
+                )
+            val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+
+            val inputFieldPlaceables =
+                inputFieldMeasurables.fastMap { it.measure(looseConstraints) }
+            val inputFieldWidth = inputFieldPlaceables.fastMaxOfOrNull { it.width } ?: 0
+            val inputFieldHeight = inputFieldPlaceables.fastMaxOfOrNull { it.height } ?: 0
+
+            val contentConstraints =
+                looseConstraints
+                    .offset(vertical = -inputFieldHeight)
+                    .copy(maxWidth = inputFieldWidth)
+            val contentPlaceables = contentMeasurables.fastMap { it.measure(contentConstraints) }
+
+            val height = inputFieldHeight + (contentPlaceables.fastMaxOfOrNull { it.height } ?: 0)
+            val width =
+                max(
+                    inputFieldWidth,
+                    contentPlaceables.fastMaxOfOrNull { it.width } ?: 0,
+                )
+
+            layout(constraints.constrainWidth(width), constraints.constrainHeight(height)) {
+                inputFieldPlaceables.fastForEach { it.place(0, 0) }
+                contentPlaceables.fastForEach { it.place(0, inputFieldHeight) }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
 private fun FullScreenSearchBarLayout(
     state: SearchBarState,
     predictiveBackState: PredictiveBackState,
@@ -2224,30 +2386,30 @@
         },
     ) { measurables, constraints ->
         val predictiveBackProgress = lastInProgressValue.value.transform()
+        val collapsedWidth =
+            state.collapsedBounds.width.takeIf { it != 0 } ?: SearchBarMinWidth.roundToPx()
+        val collapsedHeight =
+            state.collapsedBounds.height.takeIf { it != 0 } ?: InputFieldHeight.roundToPx()
 
         val predictiveBackEndWidth =
             (constraints.maxWidth * SearchBarPredictiveBackMinScale)
                 .roundToInt()
-                .coerceAtLeast(state.collapsedBounds.width)
+                .coerceAtLeast(collapsedWidth)
         val predictiveBackEndHeight =
             (constraints.maxHeight * SearchBarPredictiveBackMinScale)
                 .roundToInt()
-                .coerceAtLeast(state.collapsedBounds.height)
+                .coerceAtLeast(collapsedHeight)
         val endWidth = lerp(constraints.maxWidth, predictiveBackEndWidth, predictiveBackProgress)
         val endHeight = lerp(constraints.maxHeight, predictiveBackEndHeight, predictiveBackProgress)
-        val width =
-            constraints.constrainWidth(lerp(state.collapsedBounds.width, endWidth, state.progress))
-        val height =
-            constraints.constrainHeight(
-                lerp(state.collapsedBounds.height, endHeight, state.progress)
-            )
+        val width = constraints.constrainWidth(lerp(collapsedWidth, endWidth, state.progress))
+        val height = constraints.constrainHeight(lerp(collapsedHeight, endHeight, state.progress))
 
         val surfaceMeasurable = measurables.fastFirst { it.layoutId == LayoutIdSurface }
         val surfacePlaceable = surfaceMeasurable.measure(Constraints.fixed(width, height))
 
         val inputFieldMeasurable = measurables.fastFirst { it.layoutId == LayoutIdInputField }
         val inputFieldPlaceable =
-            inputFieldMeasurable.measure(Constraints.fixed(width, state.collapsedBounds.height))
+            inputFieldMeasurable.measure(Constraints.fixed(width, collapsedHeight))
 
         val topPadding = unconsumedInsets.getTop(this@Layout) + SearchBarVerticalPadding.roundToPx()
         val bottomPadding = SearchBarVerticalPadding.roundToPx()
@@ -2343,7 +2505,7 @@
 @OptIn(ExperimentalMaterial3Api::class)
 private val SearchBarState.collapsedBounds: IntRect
     get() =
-        collapsedCoords?.let { IntRect(offset = it.positionOnScreen().round(), size = it.size) }
+        collapsedCoords?.let { IntRect(offset = it.positionInWindow().round(), size = it.size) }
             ?: IntRect.Zero
 
 private fun calculatePredictiveBackMultiplier(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt
index badfc12..4c8d388 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt
@@ -259,7 +259,10 @@
     }
 
     /** Cached shapes used in components */
-    internal var defaultToggleButtonShapesCached: ButtonShapes? = null
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    internal var defaultButtonShapesCached: ButtonShapes? = null
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+    internal var defaultToggleButtonShapesCached: ToggleButtonShapes? = null
     internal var defaultVerticalDragHandleShapesCached: DragHandleShapes? = null
 }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index 7705df8..94234e8 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -289,8 +289,6 @@
 ) {
     val state =
         remember(steps, valueRange) { SliderState(value, steps, onValueChangeFinished, valueRange) }
-
-    state.onValueChangeFinished = onValueChangeFinished
     state.onValueChange = onValueChange
     state.value = value
 
@@ -2399,23 +2397,23 @@
 
     private var valueState by mutableFloatStateOf(value)
 
-    /**
-     * [Float] that indicates the current value that the thumb currently is in respect to the track.
-     */
+    /** [Float] that indicates the value that the thumb currently is in respect to the track. */
     var value: Float
         set(newVal) {
-            val coercedValue = newVal.coerceIn(valueRange.start, valueRange.endInclusive)
-            val snappedValue =
-                snapValueToTick(
-                    coercedValue,
-                    tickFractions,
-                    valueRange.start,
-                    valueRange.endInclusive
-                )
-            valueState = snappedValue
+            valueState = calculateSnappedValue(newVal)
         }
         get() = valueState
 
+    private fun calculateSnappedValue(newVal: Float): Float {
+        val coercedValue = newVal.coerceIn(valueRange.start, valueRange.endInclusive)
+        return snapValueToTick(
+            coercedValue,
+            tickFractions,
+            valueRange.start,
+            valueRange.endInclusive
+        )
+    }
+
     override suspend fun drag(
         dragPriority: MutatePriority,
         block: suspend DragScope.() -> Unit
@@ -2448,7 +2446,7 @@
         }
     }
 
-    /** callback in which value should be updated */
+    /** Callback in which value should be updated. */
     var onValueChange: ((Float) -> Unit)? = null
 
     internal val tickFractions = stepsToTickFractions(steps)
@@ -2471,7 +2469,7 @@
                 value.coerceIn(valueRange.start, valueRange.endInclusive)
             )
 
-    internal var isDragging by mutableStateOf(false)
+    var isDragging by mutableStateOf(false)
         private set
 
     internal fun updateDimensions(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index f11dc72..4dc22d5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -17,6 +17,7 @@
 
 package androidx.compose.material3
 
+import androidx.annotation.FloatRange
 import androidx.annotation.IntRange
 import androidx.collection.IntList
 import androidx.collection.MutableIntList
@@ -165,7 +166,6 @@
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -720,6 +720,8 @@
 
 internal class AnalogTimePickerState(val state: TimePickerState) : TimePickerState by state {
 
+    var currentDiameter by mutableStateOf(0.dp)
+
     val currentAngle: Float
         get() = anim.value
 
@@ -915,18 +917,20 @@
 
 internal val AnalogTimePickerState.selectorPos: DpOffset
     get() {
-        val handleRadiusPx = ClockDialSelectorHandleContainerSize / 2
+        val scale: Float = currentDiameter / ClockDialContainerSize
+        val handleRadiusDp = (ClockDialSelectorHandleContainerSize / 2f) * scale
         val selectorLength =
             if (is24hour && this.isPm && selection == TimePickerSelectionMode.Hour) {
-                    InnerCircleRadius
+                    currentDiameter * InnerCircleToSizeRatio
                 } else {
-                    OuterCircleSizeRadius
+                    currentDiameter * OuterCircleToSizeRatio
                 }
-                .minus(handleRadiusPx)
+                .minus(handleRadiusDp)
+                .coerceAtLeast(0.dp)
 
-        val length = selectorLength + handleRadiusPx
-        val offsetX = length * cos(currentAngle) + ClockDialContainerSize / 2
-        val offsetY = length * sin(currentAngle) + ClockDialContainerSize / 2
+        val length = selectorLength + handleRadiusDp
+        val offsetX = length * cos(currentAngle) + currentDiameter / 2
+        val offsetY = length * sin(currentAngle) + currentDiameter / 2
 
         return DpOffset(offsetX, offsetY)
     }
@@ -943,9 +947,14 @@
         modifier = modifier.semantics { isTraversalGroup = true },
         horizontalAlignment = Alignment.CenterHorizontally
     ) {
-        VerticalClockDisplay(state, colors)
+        VerticalClockDisplay(state = state, colors = colors)
         Spacer(modifier = Modifier.height(ClockDisplayBottomMargin))
-        ClockFace(state, colors, autoSwitchToMinute)
+        ClockFace(
+            modifier = Modifier.size(ClockDialContainerSize),
+            state = state,
+            colors = colors,
+            autoSwitchToMinute = autoSwitchToMinute
+        )
         Spacer(modifier = Modifier.height(ClockFaceBottomMargin))
     }
 }
@@ -964,7 +973,12 @@
     ) {
         HorizontalClockDisplay(state, colors)
         Spacer(modifier = Modifier.width(ClockDisplayBottomMargin))
-        ClockFace(state, colors, autoSwitchToMinute)
+        ClockFace(
+            modifier = Modifier.then(ClockFaceSizeModifier()),
+            state,
+            colors,
+            autoSwitchToMinute
+        )
     }
 }
 
@@ -1453,9 +1467,13 @@
 
     private var offsetX = 0f
     private var offsetY = 0f
-    private var center: IntOffset = IntOffset.Zero
+    private var center: IntOffset by mutableStateOf(IntOffset.Zero)
     private val maxDist
-        get() = with(requireDensity()) { MaxDistance.toPx() }
+        get() =
+            with(requireDensity()) {
+                MaxDistance.toPx() * state.currentDiameter.roundToPx() /
+                    ClockDialContainerSize.roundToPx()
+            }
 
     private val pointerInputTapNode =
         delegate(
@@ -1498,14 +1516,20 @@
                         offsetX += dragAmount.x
                         offsetY += dragAmount.y
                         state.rotateTo(atan(offsetY - center.y, offsetX - center.x), animationSpec)
+                        state.moveSelector(
+                            x = offsetX,
+                            y = offsetY,
+                            maxDist = maxDist,
+                            center = center
+                        )
                     }
-                    state.moveSelector(offsetX, offsetY, maxDist, center)
                 }
             }
         )
 
     override fun onRemeasured(size: IntSize) {
         center = size.center
+        state.currentDiameter = with(requireDensity()) { size.width.toDp() }
     }
 
     override fun onPointerEvent(
@@ -1540,6 +1564,7 @@
 
 @Composable
 internal fun ClockFace(
+    modifier: Modifier,
     state: AnalogTimePickerState,
     colors: TimePickerColors,
     autoSwitchToMinute: Boolean
@@ -1547,7 +1572,8 @@
     // TODO Load the motionScheme tokens from the component tokens file
     Crossfade(
         modifier =
-            Modifier.background(shape = CircleShape, color = colors.clockDialColor)
+            modifier
+                .background(shape = CircleShape, color = colors.clockDialColor)
                 .then(
                     ClockDialModifier(
                         state,
@@ -1556,14 +1582,13 @@
                         MotionSchemeKeyTokens.DefaultSpatial.value()
                     )
                 )
-                .size(ClockDialContainerSize)
                 .drawSelector(state, colors),
         targetState = state.clockFaceValues,
         animationSpec = MotionSchemeKeyTokens.DefaultEffects.value()
     ) { screen ->
         CircularLayout(
             modifier = Modifier.size(ClockDialContainerSize).semantics { selectableGroup() },
-            radius = OuterCircleSizeRadius,
+            radiusToSizeRatio = OuterCircleToSizeRatio,
         ) {
             CompositionLocalProvider(
                 LocalContentColor provides colors.clockDialContentColor(false)
@@ -1589,7 +1614,7 @@
                             Modifier.layoutId(LayoutId.InnerCircle)
                                 .size(ClockDialContainerSize)
                                 .background(shape = CircleShape, color = Color.Transparent),
-                        radius = InnerCircleRadius
+                        radiusToSizeRatio = InnerCircleToSizeRatio
                     ) {
                         repeat(ExtraHours.size) { index ->
                             val innerValue = ExtraHours[index]
@@ -1615,7 +1640,9 @@
     this.drawWithContent {
         val selectorOffsetPx = Offset(state.selectorPos.x.toPx(), state.selectorPos.y.toPx())
 
-        val selectorRadius = ClockDialSelectorHandleContainerSize.toPx() / 2
+        val selectorRadius =
+            ClockDialSelectorHandleContainerSize.toPx() / 2f * state.currentDiameter.roundToPx() /
+                ClockDialContainerSize.roundToPx()
         val selectorColor = colors.selectorColor
 
         // clear out the selector section
@@ -1910,15 +1937,18 @@
     }
 }
 
-/** Distribute elements evenly on a circle of [radius] */
+/**
+ * Distribute children evenly on a circle of radius equal to the height of this layout times
+ * [radiusToSizeRatio].
+ */
 @Composable
 private fun CircularLayout(
     modifier: Modifier = Modifier,
-    radius: Dp,
+    @FloatRange(from = 0.0, to = 1.0) radiusToSizeRatio: Float,
     content: @Composable () -> Unit,
 ) {
     Layout(modifier = modifier, content = content) { measurables, constraints ->
-        val radiusPx = radius.toPx()
+        val radiusPx = constraints.maxHeight * radiusToSizeRatio
         val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0)
         val placeables =
             measurables
@@ -2006,8 +2036,8 @@
 private const val RadiansPerHour: Float = FullCircle / 12f
 private const val SeparatorZIndex = 2f
 
-private val OuterCircleSizeRadius = 101.dp
-private val InnerCircleRadius = 69.dp
+private val OuterCircleToSizeRatio: Float = 101.dp / ClockDialContainerSize
+private val InnerCircleToSizeRatio: Float = 69.dp / ClockDialContainerSize
 private val ClockDisplayBottomMargin = 36.dp
 private val ClockFaceBottomMargin = 24.dp
 private val DisplaySeparatorWidth = 24.dp
@@ -2022,6 +2052,11 @@
     MutableIntList(Hours.size).apply { Hours.forEach { add((it % 12 + 12)) } }
 private val PeriodToggleMargin = 12.dp
 
+private val TimePickerMaxHeight = 384.dp
+private val TimePickerMidHeight = 330.dp
+private val ClockDialMidContainerSize = 238.dp
+private val ClockDialMinContainerSize = 200.dp
+
 /**
  * Measure the composable with 0,0 so that it stays on the screen. Necessary to correctly handle
  * focus
@@ -2060,3 +2095,22 @@
         return visible == otherModifier.visible
     }
 }
+
+internal class ClockFaceSizeModifier : LayoutModifier {
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        var max = constraints.maxHeight.toDp()
+        val size =
+            when {
+                max >= TimePickerMaxHeight -> ClockDialContainerSize
+                max >= TimePickerMidHeight -> ClockDialMidContainerSize
+                else -> ClockDialMinContainerSize
+            }.roundToPx()
+
+        val placeable = measurable.measure(Constraints.fixed(size, size))
+        return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+    }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ToggleButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ToggleButton.kt
index 2f4b4f4..3052150 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ToggleButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ToggleButton.kt
@@ -67,7 +67,7 @@
  * [checked]'s value. It also morphs between the three shapes provided in [shapes] depending on the
  * state of the interaction with the toggle button as long as the three shapes provided our
  * [CornerBasedShape]s. If a shape in [shapes] isn't a [CornerBasedShape], then toggle button will
- * toggle between the [ButtonShapes] according to user interaction.
+ * toggle between the [ToggleButtonShapes] according to user interaction.
  *
  * TODO link to an image when available
  *
@@ -93,7 +93,7 @@
  * @param enabled controls the enabled state of this toggle button. When `false`, this component
  *   will not respond to user input, and it will appear visually disabled and disabled to
  *   accessibility services.
- * @param shapes the [ButtonShapes] that the toggle button will morph between depending on the
+ * @param shapes the [ToggleButtonShapes] that the toggle button will morph between depending on the
  *   user's interaction with the toggle button.
  * @param colors [ToggleButtonColors] that will be used to resolve the colors used for this toggle
  *   button in different states. See [ToggleButtonDefaults.toggleButtonColors].
@@ -117,7 +117,7 @@
     onCheckedChange: (Boolean) -> Unit,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shapes: ButtonShapes = ToggleButtonDefaults.shapes(),
+    shapes: ToggleButtonShapes = ToggleButtonDefaults.shapes(),
     colors: ToggleButtonColors = ToggleButtonDefaults.toggleButtonColors(),
     elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
     border: BorderStroke? = null,
@@ -197,7 +197,7 @@
  * [checked]'s value. It also morphs between the three shapes provided in [shapes] depending on the
  * state of the interaction with the toggle button as long as the three shapes provided our
  * [CornerBasedShape]s. If a shape in [shapes] isn't a [CornerBasedShape], then toggle button will
- * toggle between the [ButtonShapes] according to user interaction.
+ * toggle between the [ToggleButtonShapes] according to user interaction.
  *
  * TODO link to an image when available
  *
@@ -214,7 +214,7 @@
  * @param enabled controls the enabled state of this toggle button. When `false`, this component
  *   will not respond to user input, and it will appear visually disabled and disabled to
  *   accessibility services.
- * @param shapes the [ButtonShapes] that the toggle button will morph between depending on the
+ * @param shapes the [ToggleButtonShapes] that the toggle button will morph between depending on the
  *   user's interaction with the toggle button.
  * @param colors [ToggleButtonColors] that will be used to resolve the colors used for this toggle
  *   button in different states. See [ToggleButtonDefaults.elevatedToggleButtonColors].
@@ -238,7 +238,7 @@
     onCheckedChange: (Boolean) -> Unit,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shapes: ButtonShapes = ToggleButtonDefaults.shapes(),
+    shapes: ToggleButtonShapes = ToggleButtonDefaults.shapes(),
     colors: ToggleButtonColors = ToggleButtonDefaults.elevatedToggleButtonColors(),
     elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
     border: BorderStroke? = null,
@@ -267,7 +267,7 @@
  * [checked]'s value. It also morphs between the three shapes provided in [shapes] depending on the
  * state of the interaction with the toggle button as long as the three shapes provided our
  * [CornerBasedShape]s. If a shape in [shapes] isn't a [CornerBasedShape], then toggle button will
- * toggle between the [ButtonShapes] according to user interaction.
+ * toggle between the [ToggleButtonShapes] according to user interaction.
  *
  * TODO link to an image when available
  *
@@ -287,7 +287,7 @@
  * @param enabled controls the enabled state of this toggle button. When `false`, this component
  *   will not respond to user input, and it will appear visually disabled and disabled to
  *   accessibility services.
- * @param shapes the [ButtonShapes] that the toggle button will morph between depending on the
+ * @param shapes the [ToggleButtonShapes] that the toggle button will morph between depending on the
  *   user's interaction with the toggle button.
  * @param colors [ToggleButtonColors] that will be used to resolve the colors used for this toggle
  *   button in different states. See [ToggleButtonDefaults.tonalToggleButtonColors].
@@ -311,7 +311,7 @@
     onCheckedChange: (Boolean) -> Unit,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shapes: ButtonShapes = ToggleButtonDefaults.shapes(),
+    shapes: ToggleButtonShapes = ToggleButtonDefaults.shapes(),
     colors: ToggleButtonColors = ToggleButtonDefaults.tonalToggleButtonColors(),
     elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(),
     border: BorderStroke? = null,
@@ -340,7 +340,7 @@
  * [checked]'s value. It also morphs between the three shapes provided in [shapes] depending on the
  * state of the interaction with the toggle button as long as the three shapes provided our
  * [CornerBasedShape]s. If a shape in [shapes] isn't a [CornerBasedShape], then toggle button will
- * toggle between the [ButtonShapes] according to user interaction.
+ * toggle between the [ToggleButtonShapes] according to user interaction.
  *
  * TODO link to an image when available
  *
@@ -358,7 +358,7 @@
  * @param enabled controls the enabled state of this toggle button. When `false`, this component
  *   will not respond to user input, and it will appear visually disabled and disabled to
  *   accessibility services.
- * @param shapes the [ButtonShapes] that the toggle button will morph between depending on the
+ * @param shapes the [ToggleButtonShapes] that the toggle button will morph between depending on the
  *   user's interaction with the toggle button.
  * @param colors [ToggleButtonColors] that will be used to resolve the colors used for this toggle
  *   button in different states. See [ToggleButtonDefaults.outlinedToggleButtonColors].
@@ -382,7 +382,7 @@
     onCheckedChange: (Boolean) -> Unit,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shapes: ButtonShapes = ToggleButtonDefaults.shapes(),
+    shapes: ToggleButtonShapes = ToggleButtonDefaults.shapes(),
     colors: ToggleButtonColors = ToggleButtonDefaults.outlinedToggleButtonColors(),
     elevation: ButtonElevation? = null,
     border: BorderStroke? = if (!checked) ButtonDefaults.outlinedButtonBorder(enabled) else null,
@@ -439,36 +439,36 @@
         )
 
     /**
-     * Creates a [ButtonShapes] that represents the default shape, pressedShape, and checkedShape
-     * used in a [ToggleButton].
+     * Creates a [ToggleButtonShapes] that represents the default shape, pressedShape, and
+     * checkedShape used in a [ToggleButton].
      */
-    @Composable fun shapes() = MaterialTheme.shapes.defaultShapes
+    @Composable fun shapes() = MaterialTheme.shapes.defaultToggleButtonShapes
 
     /**
-     * Creates a [ButtonShapes] that represents the default shape, pressedShape, and checkedShape
-     * used in a [ToggleButton] and its variants.
+     * Creates a [ToggleButtonShapes] that represents the default shape, pressedShape, and
+     * checkedShape used in a [ToggleButton] and its variants.
      *
-     * @param shape the unchecked shape for [ButtonShapes]
-     * @param pressedShape the unchecked shape for [ButtonShapes]
-     * @param checkedShape the unchecked shape for [ButtonShapes]
+     * @param shape the unchecked shape for [ToggleButtonShapes]
+     * @param pressedShape the unchecked shape for [ToggleButtonShapes]
+     * @param checkedShape the unchecked shape for [ToggleButtonShapes]
      */
     @Composable
     fun shapes(
         shape: Shape? = null,
         pressedShape: Shape? = null,
         checkedShape: Shape? = null
-    ): ButtonShapes =
-        MaterialTheme.shapes.defaultShapes.copy(
+    ): ToggleButtonShapes =
+        MaterialTheme.shapes.defaultToggleButtonShapes.copy(
             shape = shape,
             pressedShape = pressedShape,
             checkedShape = checkedShape
         )
 
-    internal val Shapes.defaultShapes: ButtonShapes
+    internal val Shapes.defaultToggleButtonShapes: ToggleButtonShapes
         @Composable
         get() {
             return defaultToggleButtonShapesCached
-                ?: ButtonShapes(
+                ?: ToggleButtonShapes(
                         shape = shape,
                         pressedShape = pressedShape,
                         checkedShape = checkedShape
@@ -885,14 +885,16 @@
  * @property pressedShape is the pressed shape.
  * @property checkedShape is the checked shape.
  */
-class ButtonShapes(val shape: Shape, val pressedShape: Shape, val checkedShape: Shape) {
-    /** Returns a copy of this ButtonShapes, optionally overriding some of the values. */
+@ExperimentalMaterial3ExpressiveApi
+@Immutable
+class ToggleButtonShapes(val shape: Shape, val pressedShape: Shape, val checkedShape: Shape) {
+    /** Returns a copy of this ToggleButtonShapes, optionally overriding some of the values. */
     fun copy(
         shape: Shape? = this.shape,
         pressedShape: Shape? = this.pressedShape,
         checkedShape: Shape? = this.checkedShape
     ) =
-        ButtonShapes(
+        ToggleButtonShapes(
             shape = shape.takeOrElse { this.shape },
             pressedShape = pressedShape.takeOrElse { this.pressedShape },
             checkedShape = checkedShape.takeOrElse { this.checkedShape }
@@ -902,7 +904,7 @@
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
-        if (other == null || other !is ButtonShapes) return false
+        if (other == null || other !is ToggleButtonShapes) return false
 
         if (shape != other.shape) return false
         if (pressedShape != other.pressedShape) return false
@@ -920,15 +922,17 @@
     }
 }
 
-internal val ButtonShapes.hasRoundedCornerShapes: Boolean
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+internal val ToggleButtonShapes.hasRoundedCornerShapes: Boolean
     get() =
         shape is RoundedCornerShape &&
             pressedShape is RoundedCornerShape &&
             checkedShape is RoundedCornerShape
 
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun shapeByInteraction(
-    shapes: ButtonShapes,
+    shapes: ToggleButtonShapes,
     pressed: Boolean,
     checked: Boolean,
     animationSpec: FiniteAnimationSpec<Float>
@@ -938,7 +942,9 @@
             shapes.pressedShape
         } else if (checked) {
             shapes.checkedShape
-        } else shapes.shape
+        } else {
+            shapes.shape
+        }
 
     if (shapes.hasRoundedCornerShapes)
         return key(shapes) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.kt
index e03821f..9986bf1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.kt
@@ -25,9 +25,11 @@
  * @param listenToTouchExplorationState whether to track the enabled/disabled state of touch
  *   exploration (i.e. TalkBack)
  * @param listenToSwitchAccessState whether to track the enabled/disabled state of Switch Access
+ * @param listenToVoiceAccessState whether to track the enabled/disabled state of Voice Access
  */
 @Composable
 internal expect fun rememberAccessibilityServiceState(
     listenToTouchExplorationState: Boolean = true,
     listenToSwitchAccessState: Boolean = true,
+    listenToVoiceAccessState: Boolean = true,
 ): State<Boolean>
diff --git a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.commonStubs.kt b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.commonStubs.kt
index e68e24c..6654fd6 100644
--- a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.commonStubs.kt
+++ b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.commonStubs.kt
@@ -24,4 +24,5 @@
 internal actual fun rememberAccessibilityServiceState(
     listenToTouchExplorationState: Boolean,
     listenToSwitchAccessState: Boolean,
+    listenToVoiceAccessState: Boolean,
 ): State<Boolean> = implementedInJetBrainsFork()
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index df41707..d1c6939 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -46,7 +46,6 @@
                 implementation(libs.kotlinStdlibCommon)
                 api(libs.kotlinCoroutinesCore)
                 implementation(project(":collection:collection"))
-                implementation(project(":performance:performance-annotation"))
             }
         }
 
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
index 243417a..5998d77 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
@@ -242,18 +242,6 @@
         withContext(TestMonotonicFrameClock(this)) { testBody() }
     }
 
-inline fun BenchmarkRule.measureRepeatedSuspendable(block: BenchmarkRule.Scope.() -> Unit) {
-    // Note: this is an extension function to discourage calling from Java.
-
-    // Extract members to locals, to ensure we check #applied, and we don't hit accessors
-    val localState = getState()
-    val localScope = scope
-
-    while (localState.keepRunningInline()) {
-        block(localScope)
-    }
-}
-
 fun ControlledComposition.performRecompose(
     readObserver: (Any) -> Unit,
     writeObserver: (Any) -> Unit
diff --git a/compose/runtime/runtime/proguard-rules.pro b/compose/runtime/runtime/proguard-rules.pro
index 400440d..6e78193 100644
--- a/compose/runtime/runtime/proguard-rules.pro
+++ b/compose/runtime/runtime/proguard-rules.pro
@@ -17,7 +17,7 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.runtime.** {
+-keep,allowshrinking,allowobfuscation class androidx.compose.runtime.** {
     # java.lang.Void == methods that return Nothing
     static void throw*Exception(...);
     static void throw*ExceptionForNullCheck(...);
@@ -31,7 +31,3 @@
     static void compose*RuntimeError(...);
     static java.lang.Void compose*RuntimeError(...);
 }
-
--keepclassmembers class * {
-    @dalvik.annotation.optimization.NeverInline *;
-}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index afd4db85..a295937 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -23,7 +23,6 @@
 import androidx.collection.MutableIntSet
 import androidx.collection.MutableObjectList
 import androidx.collection.mutableIntListOf
-import androidx.compose.runtime.collection.fastCopyInto
 import androidx.compose.runtime.platform.makeSynchronizedObject
 import androidx.compose.runtime.platform.synchronized
 import androidx.compose.runtime.snapshots.fastAny
@@ -2123,7 +2122,7 @@
         //  4) copy the slots to their new location
         if (moveDataLen > 0) {
             val slots = slots
-            slots.fastCopyInto(
+            slots.copyInto(
                 destination = slots,
                 destinationOffset = destinationSlot,
                 startIndex = dataIndexToDataAddress(dataStart + moveDataLen),
@@ -2207,7 +2206,7 @@
             )
             val slots = toWriter.slots
             val currentSlot = toWriter.currentSlot
-            fromWriter.slots.fastCopyInto(
+            fromWriter.slots.copyInto(
                 destination = slots,
                 destinationOffset = currentSlot,
                 startIndex = sourceSlotsStart,
@@ -2677,7 +2676,7 @@
             val slots = slots
             if (index < gapStart) {
                 // move the gap down to index by shifting the data up.
-                slots.fastCopyInto(
+                slots.copyInto(
                     destination = slots,
                     destinationOffset = index + gapLen,
                     startIndex = index,
@@ -2685,7 +2684,7 @@
                 )
             } else {
                 // Shift the data down, leaving the gap at index
-                slots.fastCopyInto(
+                slots.copyInto(
                     destination = slots,
                     destinationOffset = gapStart,
                     startIndex = gapStart + gapLen,
@@ -2831,13 +2830,13 @@
                 val newGapEndAddress = gapStart + newGapLen
 
                 // Copy the old arrays into the new arrays
-                slots.fastCopyInto(
+                slots.copyInto(
                     destination = newData,
                     destinationOffset = 0,
                     startIndex = 0,
                     endIndex = gapStart
                 )
-                slots.fastCopyInto(
+                slots.copyInto(
                     destination = newData,
                     destinationOffset = newGapEndAddress,
                     startIndex = oldGapEndAddress,
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Stack.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Stack.kt
index c2e25cd..03989dd 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Stack.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Stack.kt
@@ -14,13 +14,9 @@
  * limitations under the License.
  */
 
-@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
-
 package androidx.compose.runtime
 
-import kotlin.jvm.JvmField
 import kotlin.jvm.JvmInline
-import kotlin.math.min
 
 @JvmInline
 internal value class Stack<T>(private val backing: ArrayList<T> = ArrayList()) {
@@ -46,33 +42,22 @@
 }
 
 internal class IntStack {
-    @JvmField internal var slots = IntArray(10)
-    @JvmField internal var tos = 0
+    private var slots = IntArray(10)
+    private var tos = 0
 
-    inline val size: Int
+    val size: Int
         get() = tos
 
-    @dalvik.annotation.optimization.NeverInline
-    private fun resize(): IntArray {
-        val copy = slots.copyOf(slots.size * 2)
-        slots = copy
-        return copy
-    }
-
     fun push(value: Int) {
-        var slots = slots
         if (tos >= slots.size) {
-            slots = resize()
+            slots = slots.copyOf(slots.size * 2)
         }
         slots[tos++] = value
     }
 
     fun pop(): Int = slots[--tos]
 
-    fun peekOr(default: Int): Int {
-        val index = tos - 1
-        return if (index >= 0) slots[index] else default
-    }
+    fun peekOr(default: Int): Int = if (tos > 0) peek() else default
 
     fun peek() = slots[tos - 1]
 
@@ -80,20 +65,16 @@
 
     fun peek(index: Int) = slots[index]
 
-    inline fun isEmpty() = tos == 0
+    fun isEmpty() = tos == 0
 
-    inline fun isNotEmpty() = tos != 0
+    fun isNotEmpty() = tos != 0
 
     fun clear() {
         tos = 0
     }
 
     fun indexOf(value: Int): Int {
-        val slots = slots
-        val end = min(slots.size, tos)
-        for (i in 0 until end) {
-            if (slots[i] == value) return i
-        }
+        for (i in 0 until tos) if (slots[i] == value) return i
         return -1
     }
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
index 6a1339b..f250bc8 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
@@ -24,10 +24,8 @@
 import androidx.compose.runtime.RememberManager
 import androidx.compose.runtime.SlotWriter
 import androidx.compose.runtime.changelist.Operation.ObjectParameter
-import androidx.compose.runtime.collection.fastCopyInto
 import androidx.compose.runtime.debugRuntimeCheck
 import androidx.compose.runtime.requirePrecondition
-import dalvik.annotation.optimization.NeverInline
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.InvocationKind.EXACTLY_ONCE
 import kotlin.contracts.contract
@@ -135,12 +133,11 @@
         return (currentSize + resizeAmount).coerceAtLeast(requiredSize)
     }
 
-    @NeverInline
     private fun resizeOpCodes() {
         val resizeAmount = opCodesSize.coerceAtMost(OperationsMaxResizeAmount)
         @Suppress("UNCHECKED_CAST")
         val newOpCodes = arrayOfNulls<Operation>(opCodesSize + resizeAmount) as Array<Operation>
-        opCodes = opCodes.fastCopyInto(newOpCodes, 0, 0, opCodesSize)
+        opCodes = opCodes.copyInto(newOpCodes, 0, 0, opCodesSize)
     }
 
     private inline fun ensureIntArgsSizeAtLeast(requiredSize: Int) {
@@ -150,7 +147,6 @@
         }
     }
 
-    @NeverInline
     private fun resizeIntArgs(currentSize: Int, requiredSize: Int) {
         val newIntArgs = IntArray(determineNewSize(currentSize, requiredSize))
         intArgs.copyInto(newIntArgs, 0, 0, currentSize)
@@ -164,10 +160,9 @@
         }
     }
 
-    @NeverInline
     private fun resizeObjectArgs(currentSize: Int, requiredSize: Int) {
         val newObjectArgs = arrayOfNulls<Any>(determineNewSize(currentSize, requiredSize))
-        objectArgs.fastCopyInto(newObjectArgs, 0, 0, currentSize)
+        objectArgs.copyInto(newObjectArgs, 0, 0, currentSize)
         objectArgs = newObjectArgs
     }
 
@@ -296,7 +291,7 @@
         other.pushOp(op)
 
         // Move the objects then null out our contents
-        objectArgs.fastCopyInto(
+        objectArgs.copyInto(
             destination = other.objectArgs,
             destinationOffset = other.objectArgsSize - op.objects,
             startIndex = objectArgsSize - op.objects,
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.kt
deleted file mode 100644
index 03c2546..0000000
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.kt
+++ /dev/null
@@ -1,28 +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.compose.runtime.collection
-
-/**
- * Equivalent of Array.copyInto() with an implementation designed to avoid unnecessary null checks
- * and exception throws on Android after inlining.
- */
-internal expect fun <T> Array<out T>.fastCopyInto(
-    destination: Array<T>,
-    destinationOffset: Int,
-    startIndex: Int,
-    endIndex: Int
-): Array<T>
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/MutableVector.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/MutableVector.kt
index 8144a7b..1315b32 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/MutableVector.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/MutableVector.kt
@@ -18,7 +18,6 @@
 
 package androidx.compose.runtime.collection
 
-import dalvik.annotation.optimization.NeverInline
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
@@ -67,7 +66,7 @@
         ensureCapacity(size + 1)
         val content = content
         if (index != size) {
-            content.fastCopyInto(
+            content.copyInto(
                 destination = content,
                 destinationOffset = index + 1,
                 startIndex = index,
@@ -84,13 +83,12 @@
      */
     fun addAll(index: Int, elements: List<T>): Boolean {
         if (elements.isEmpty()) return false
-        val elementsSize = elements.size
-        ensureCapacity(size + elementsSize)
+        ensureCapacity(size + elements.size)
         val content = content
         if (index != size) {
-            content.fastCopyInto(
+            content.copyInto(
                 destination = content,
-                destinationOffset = index + elementsSize,
+                destinationOffset = index + elements.size,
                 startIndex = index,
                 endIndex = size
             )
@@ -98,7 +96,7 @@
         for (i in elements.indices) {
             content[index + i] = elements[i]
         }
-        size += elementsSize
+        size += elements.size
         return true
     }
 
@@ -107,25 +105,24 @@
      * that are in the way.
      */
     fun addAll(index: Int, elements: MutableVector<T>): Boolean {
-        val elementsSize = elements.size
-        if (elementsSize == 0) return false
-        ensureCapacity(size + elementsSize)
+        if (elements.isEmpty()) return false
+        ensureCapacity(size + elements.size)
         val content = content
         if (index != size) {
-            content.fastCopyInto(
+            content.copyInto(
                 destination = content,
-                destinationOffset = index + elementsSize,
+                destinationOffset = index + elements.size,
                 startIndex = index,
                 endIndex = size
             )
         }
-        elements.content.fastCopyInto(
+        elements.content.copyInto(
             destination = content,
             destinationOffset = index,
             startIndex = 0,
-            endIndex = elementsSize
+            endIndex = elements.size
         )
-        size += elementsSize
+        size += elements.size
         return true
     }
 
@@ -150,13 +147,12 @@
      * [MutableVector] was changed.
      */
     fun addAll(@Suppress("ArrayReturn") elements: Array<T>): Boolean {
-        val elementsSize = elements.size
-        if (elementsSize == 0) {
+        if (elements.isEmpty()) {
             return false
         }
-        ensureCapacity(size + elementsSize)
-        elements.fastCopyInto(destination = content, destinationOffset = size, 0, elementsSize)
-        size += elementsSize
+        ensureCapacity(size + elements.size)
+        elements.copyInto(destination = content, destinationOffset = size)
+        size += elements.size
         return true
     }
 
@@ -166,19 +162,18 @@
      */
     fun addAll(index: Int, elements: Collection<T>): Boolean {
         if (elements.isEmpty()) return false
-        val elementsSize = elements.size
-        ensureCapacity(size + elementsSize)
+        ensureCapacity(size + elements.size)
         val content = content
         if (index != size) {
-            content.fastCopyInto(
+            content.copyInto(
                 destination = content,
-                destinationOffset = index + elementsSize,
+                destinationOffset = index + elements.size,
                 startIndex = index,
                 endIndex = size
             )
         }
         elements.forEachIndexed { i, item -> content[index + i] = item }
-        size += elementsSize
+        size += elements.size
         return true
     }
 
@@ -292,14 +287,13 @@
         }
     }
 
-    @NeverInline
     @PublishedApi
     internal fun resizeStorage(capacity: Int) {
         val oldContent = content
         val oldSize = oldContent.size
         val newSize = max(capacity, oldSize * 2)
         val newContent = arrayOfNulls<Any?>(newSize) as Array<T?>
-        oldContent.fastCopyInto(newContent, 0, 0, oldSize)
+        oldContent.copyInto(newContent, 0, 0, oldSize)
         content = newContent
     }
 
@@ -700,7 +694,7 @@
         val content = content
         val item = content[index] as T
         if (index != lastIndex) {
-            content.fastCopyInto(
+            content.copyInto(
                 destination = content,
                 destinationOffset = index,
                 startIndex = index + 1,
@@ -716,7 +710,7 @@
     fun removeRange(start: Int, end: Int) {
         if (end > start) {
             if (end < size) {
-                content.fastCopyInto(
+                content.copyInto(
                     destination = content,
                     destinationOffset = start,
                     startIndex = end,
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotWeakSet.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotWeakSet.kt
index e70d6fd..66075f0a 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotWeakSet.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotWeakSet.kt
@@ -17,7 +17,6 @@
 package androidx.compose.runtime.snapshots
 
 import androidx.compose.runtime.TestOnly
-import androidx.compose.runtime.collection.fastCopyInto
 import androidx.compose.runtime.internal.WeakReference
 import androidx.compose.runtime.internal.identityHashCode
 
@@ -71,18 +70,13 @@
             val newCapacity = capacity * 2
             val newValues = arrayOfNulls<WeakReference<T>?>(newCapacity)
             val newHashes = IntArray(newCapacity)
-            values.fastCopyInto(
+            values.copyInto(
                 destination = newValues,
                 destinationOffset = insertIndex + 1,
                 startIndex = insertIndex,
                 endIndex = size
             )
-            values.fastCopyInto(
-                destination = newValues,
-                destinationOffset = 0,
-                startIndex = 0,
-                endIndex = insertIndex
-            )
+            values.copyInto(destination = newValues, endIndex = insertIndex)
             hashes.copyInto(
                 destination = newHashes,
                 destinationOffset = insertIndex + 1,
@@ -93,7 +87,7 @@
             values = newValues
             hashes = newHashes
         } else {
-            values.fastCopyInto(
+            values.copyInto(
                 destination = values,
                 destinationOffset = insertIndex + 1,
                 startIndex = insertIndex,
diff --git a/compose/ui/ui-graphics/proguard-rules.pro b/compose/ui/ui-graphics/proguard-rules.pro
index 72f4a6c..67d118b 100644
--- a/compose/ui/ui-graphics/proguard-rules.pro
+++ b/compose/ui/ui-graphics/proguard-rules.pro
@@ -15,7 +15,7 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.**.* {
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
     static void throw*Exception(...);
     static void throw*ExceptionForNullCheck(...);
     # For methods returning Nothing
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
index 8a2ff33..1d3b503 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
@@ -82,7 +82,8 @@
     // structure beyond what can be found in various descriptions of binary search
     // trees and red/black trees
 
-    @JvmField internal val terminator = Node(Float.MAX_VALUE, Float.MIN_VALUE, null, TreeColorBlack)
+    @JvmField
+    internal val terminator = Node(Float.MAX_VALUE, Float.MIN_VALUE, null, TreeColor.Black)
     @JvmField internal var root = terminator
     @JvmField internal val stack = ArrayList<Node>()
 
@@ -202,7 +203,7 @@
      * @param data Data to associate with the interval
      */
     fun addInterval(start: Float, end: Float, data: T?) {
-        val node = Node(start, end, data, TreeColorRed)
+        val node = Node(start, end, data, TreeColor.Red)
 
         // Update the tree without doing any balancing
         var current = root
@@ -238,44 +239,44 @@
     private fun rebalance(target: Node) {
         var node = target
 
-        while (node !== root && node.parent.color == TreeColorRed) {
+        while (node !== root && node.parent.color == TreeColor.Red) {
             val ancestor = node.parent.parent
             if (node.parent === ancestor.left) {
                 val right = ancestor.right
-                if (right.color == TreeColorRed) {
-                    right.color = TreeColorBlack
-                    node.parent.color = TreeColorBlack
-                    ancestor.color = TreeColorRed
+                if (right.color == TreeColor.Red) {
+                    right.color = TreeColor.Black
+                    node.parent.color = TreeColor.Black
+                    ancestor.color = TreeColor.Red
                     node = ancestor
                 } else {
                     if (node === node.parent.right) {
                         node = node.parent
                         rotateLeft(node)
                     }
-                    node.parent.color = TreeColorBlack
-                    ancestor.color = TreeColorRed
+                    node.parent.color = TreeColor.Black
+                    ancestor.color = TreeColor.Red
                     rotateRight(ancestor)
                 }
             } else {
                 val left = ancestor.left
-                if (left.color == TreeColorRed) {
-                    left.color = TreeColorBlack
-                    node.parent.color = TreeColorBlack
-                    ancestor.color = TreeColorRed
+                if (left.color == TreeColor.Red) {
+                    left.color = TreeColor.Black
+                    node.parent.color = TreeColor.Black
+                    ancestor.color = TreeColor.Red
                     node = ancestor
                 } else {
                     if (node === node.parent.left) {
                         node = node.parent
                         rotateRight(node)
                     }
-                    node.parent.color = TreeColorBlack
-                    ancestor.color = TreeColorRed
+                    node.parent.color = TreeColor.Black
+                    ancestor.color = TreeColor.Red
                     rotateLeft(ancestor)
                 }
             }
         }
 
-        root.color = TreeColorBlack
+        root.color = TreeColor.Black
     }
 
     private fun rotateLeft(node: Node) {
@@ -339,6 +340,11 @@
         }
     }
 
+    internal enum class TreeColor {
+        Red,
+        Black
+    }
+
     internal inner class Node(start: Float, end: Float, data: T?, var color: TreeColor) :
         Interval<T>(start, end, data) {
         var min: Float = start
@@ -372,8 +378,3 @@
         }
     }
 }
-
-private typealias TreeColor = Int
-
-private const val TreeColorRed = 0
-private const val TreeColorBlack = 1
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathGeometry.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathGeometry.kt
index 4c96e1e..53b0d54 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathGeometry.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathGeometry.kt
@@ -58,7 +58,7 @@
     // segments.
     var type = iterator.next(points)
     while (type != PathSegment.Type.Done) {
-        @Suppress("KotlinConstantConditions", "RedundantSuppression")
+        @Suppress("KotlinConstantConditions")
         when (type) {
             PathSegment.Type.Move -> {
                 if (!first) {
@@ -175,7 +175,7 @@
 
     var type = iterator.next(points)
     while (type != PathSegment.Type.Done) {
-        @Suppress("KotlinConstantConditions", "RedundantSuppression")
+        @Suppress("KotlinConstantConditions")
         when (type) {
             PathSegment.Type.Move -> {
                 if (!first && !isEmpty) {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt
index ec0cc69..3905cff 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt
@@ -633,7 +633,7 @@
  * @param r2: The third element of the vector
  * @return The first element of the resulting multiplication.
  */
-@Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+@Suppress("NOTHING_TO_INLINE")
 internal inline fun mul3x3Float3_0(lhs: FloatArray, r0: Float, r1: Float, r2: Float): Float {
     return lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2
 }
@@ -648,7 +648,7 @@
  * @param r2: The third element of the vector
  * @return The second element of the resulting multiplication.
  */
-@Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+@Suppress("NOTHING_TO_INLINE")
 internal inline fun mul3x3Float3_1(lhs: FloatArray, r0: Float, r1: Float, r2: Float): Float {
     return lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2
 }
@@ -663,7 +663,7 @@
  * @param r2: The third element of the vector
  * @return The third element of the resulting multiplication.
  */
-@Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+@Suppress("NOTHING_TO_INLINE")
 internal inline fun mul3x3Float3_2(lhs: FloatArray, r0: Float, r1: Float, r2: Float): Float {
     return lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2
 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt
index 2a81bac..7a47484 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt
@@ -230,7 +230,7 @@
                         chromaticAdaptation(
                             Adaptation.Bradford.transform,
                             srcXYZ,
-                            Illuminant.newD50Xyz()
+                            Illuminant.D50Xyz.copyOf()
                         )
                     transform = mul3x3(srcAdaptation, source.transform)
                 }
@@ -240,7 +240,7 @@
                         chromaticAdaptation(
                             Adaptation.Bradford.transform,
                             dstXYZ,
-                            Illuminant.newD50Xyz()
+                            Illuminant.D50Xyz.copyOf()
                         )
                     inverseTransform = inverse3x3(mul3x3(dstAdaptation, destination.transform))
                 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Illuminant.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Illuminant.kt
index 0bf3461..b3598c3 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Illuminant.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Illuminant.kt
@@ -73,6 +73,4 @@
     val E = WhitePoint(0.33333f, 0.33333f)
 
     internal val D50Xyz = floatArrayOf(0.964212f, 1.0f, 0.825188f)
-
-    internal fun newD50Xyz() = floatArrayOf(0.964212f, 1.0f, 0.825188f)
 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt
index e9d3032..06f2891 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+@file:Suppress("NOTHING_TO_INLINE")
 
 package androidx.compose.ui.graphics.colorspace
 
@@ -853,9 +853,10 @@
         var result = super.hashCode()
         result = 31 * result + whitePoint.hashCode()
         result = 31 * result + primaries.contentHashCode()
-        result = 31 * result + (if (min != 0.0f) min.toBits() else 0)
-        result = 31 * result + (if (max != 0.0f) max.toBits() else 0)
-        result = (31 * result + (transferParameters?.hashCode() ?: 0))
+        result = 31 * result + (if (min != +0.0f) min.toBits() else 0)
+        result = 31 * result + (if (max != +0.0f) max.toBits() else 0)
+        result =
+            (31 * result + if (transferParameters != null) transferParameters.hashCode() else 0)
         if (transferParameters == null) {
             result = 31 * result + oetfOrig.hashCode()
             result = 31 * result + eotfOrig.hashCode()
diff --git a/compose/ui/ui-text/proguard-rules.pro b/compose/ui/ui-text/proguard-rules.pro
index 72f4a6c..67d118b 100644
--- a/compose/ui/ui-text/proguard-rules.pro
+++ b/compose/ui/ui-text/proguard-rules.pro
@@ -15,7 +15,7 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.**.* {
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
     static void throw*Exception(...);
     static void throw*ExceptionForNullCheck(...);
     # For methods returning Nothing
diff --git a/compose/ui/ui-unit/proguard-rules.pro b/compose/ui/ui-unit/proguard-rules.pro
index 72f4a6c..67d118b 100644
--- a/compose/ui/ui-unit/proguard-rules.pro
+++ b/compose/ui/ui-unit/proguard-rules.pro
@@ -15,7 +15,7 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.**.* {
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
     static void throw*Exception(...);
     static void throw*ExceptionForNullCheck(...);
     # For methods returning Nothing
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
index 80e1938..79ba2da 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
@@ -368,16 +368,10 @@
 private const val MinFocusBits = 16
 private const val MaxAllowedForMinFocusBits = (1 shl (31 - MinFocusBits)) - 2
 
-/** The mask to use for the focused dimension when there is minimal focus. */
-private const val MinFocusMask = 0xFFFF // 64K (16 bits)
-
 /** The number of bits used for the non-focused dimension when there is minimal focus. */
 private const val MinNonFocusBits = 15
 private const val MaxAllowedForMinNonFocusBits = (1 shl (31 - MinNonFocusBits)) - 2
 
-/** The mask to use for the non-focused dimension when there is minimal focus. */
-private const val MinNonFocusMask = 0x7FFF // 32K (15 bits)
-
 /** The number of bits to use for the focused dimension when there is maximal focus. */
 private const val MaxFocusBits = 18
 private const val MaxAllowedForMaxFocusBits = (1 shl (31 - MaxFocusBits)) - 2
@@ -389,9 +383,6 @@
 private const val MaxNonFocusBits = 13
 private const val MaxAllowedForMaxNonFocusBits = (1 shl (31 - MaxNonFocusBits)) - 2
 
-/** The mask to use for the non-focused dimension when there is maximal focus. */
-private const val MaxNonFocusMask = 0x1FFF // 8K (13 bits)
-
 // 0xFFFFFFFE_00000003UL.toLong(), written as a signed value to declare it const
 @PublishedApi internal const val MaxDimensionsAndFocusMask = -0x00000001_FFFFFFFDL
 
@@ -452,22 +443,35 @@
 }
 
 internal fun bitsNeedForSizeUnchecked(size: Int): Int {
+    // We could look at the value of size itself, for instance by doing:
+    // when {
+    //     size < MaxNonFocusMask -> MaxNonFocusBits
+    //     ...
+    // }
+    // but the following solution saves a few instructions by avoiding
+    // multiple moves to load large constants
+    val bits = (size + 1).countLeadingZeroBits()
     return when {
-        size < MaxNonFocusMask -> MaxNonFocusBits
-        size < MinNonFocusMask -> MinNonFocusBits
-        size < MinFocusMask -> MinFocusBits
-        size < MaxFocusMask -> MaxFocusBits
+        bits >= 32 - MaxNonFocusBits -> MaxNonFocusBits
+        bits >= 32 - MinNonFocusBits -> MinNonFocusBits
+        bits >= 32 - MinFocusBits -> MinFocusBits
+        bits >= 32 - MaxFocusBits -> MaxFocusBits
         else -> 255
     }
 }
 
 private inline fun maxAllowedForSize(size: Int): Int {
+    // See comment in bitsNeedForSizeUnchecked()
+    // Note: the return value in every case is `1 shl (31 - bits) - 2`
+    // However, computing the value instead of using constants uses more
+    // instructions, so not worth it
+    val bits = (size + 1).countLeadingZeroBits()
+    if (bits <= 13) throwInvalidConstraintsSizeException(size)
     return when {
-        size < MaxNonFocusMask -> MaxAllowedForMaxNonFocusBits
-        size < MinNonFocusMask -> MaxAllowedForMinNonFocusBits
-        size < MinFocusMask -> MaxAllowedForMinFocusBits
-        size < MaxFocusMask -> MaxAllowedForMaxFocusBits
-        else -> throwInvalidConstraintsSizeException(size)
+        bits >= 32 - MaxNonFocusBits -> MaxAllowedForMaxNonFocusBits
+        bits >= 32 - MinNonFocusBits -> MaxAllowedForMinNonFocusBits
+        bits >= 32 - MinFocusBits -> MaxAllowedForMinFocusBits
+        else -> MaxAllowedForMaxFocusBits
     }
 }
 
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Density.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Density.kt
index cbefe8a..5b3e3c5 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Density.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Density.kt
@@ -65,7 +65,7 @@
      */
     @Stable
     fun TextUnit.toPx(): Float {
-        checkPrecondition(type == TextUnitType.Sp) { "Only Sp can convert to Px" }
+        check(type == TextUnitType.Sp) { "Only Sp can convert to Px" }
         return toDp().toPx()
     }
 
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
index e5cac56..a88b3c0 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
@@ -83,10 +83,7 @@
         /** Infinite dp dimension. */
         @Stable val Infinity = Dp(Float.POSITIVE_INFINITY)
 
-        /**
-         * Constant that means unspecified Dp. Instead of comparing a [Dp] value to this constant,
-         * consider using [isSpecified] and [isUnspecified] instead.
-         */
+        /** Constant that means unspecified Dp */
         @Stable val Unspecified = Dp(Float.NaN)
     }
 }
diff --git a/compose/ui/ui-util/proguard-rules.pro b/compose/ui/ui-util/proguard-rules.pro
index 72f4a6c..67d118b 100644
--- a/compose/ui/ui-util/proguard-rules.pro
+++ b/compose/ui/ui-util/proguard-rules.pro
@@ -15,7 +15,7 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.**.* {
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
     static void throw*Exception(...);
     static void throw*ExceptionForNullCheck(...);
     # For methods returning Nothing
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/build.gradle b/compose/ui/ui/build.gradle
index 3a1ada1..73d3551 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -24,6 +24,7 @@
 
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
+import androidx.build.KmpPlatformsKt
 import androidx.build.PlatformIdentifier
 
 import static androidx.inspection.gradle.InspectionPluginKt.packageInspector
@@ -59,8 +60,6 @@
                 api(project(":compose:ui:ui-util"))
 
                 api("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
-
-                implementation(project(":performance:performance-annotation"))
             }
         }
 
diff --git a/compose/ui/ui/proguard-rules.pro b/compose/ui/ui/proguard-rules.pro
index 1f592ff3..fa5b4c3 100644
--- a/compose/ui/ui/proguard-rules.pro
+++ b/compose/ui/ui/proguard-rules.pro
@@ -35,7 +35,7 @@
 # Keep all the functions created to throw an exception. We don't want these functions to be
 # inlined in any way, which R8 will do by default. The whole point of these functions is to
 # reduce the amount of code generated at the call site.
--keepclassmembers,allowshrinking,allowobfuscation class androidx.compose.**.* {
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
     static void throw*Exception(...);
     static void throw*ExceptionForNullCheck(...);
     # For methods returning Nothing
@@ -52,7 +52,3 @@
 -keepnames class androidx.compose.ui.input.pointer.PointerInputEventHandler {
     *;
 }
-
--keepclassmembers class * {
-    @dalvik.annotation.optimization.NeverInline *;
-}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 164d5628..7a030d67 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -210,6 +210,7 @@
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SET_TEXT
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_IME_ENTER
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.FOCUS_INPUT
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_FLOAT
 import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
@@ -2581,6 +2582,48 @@
     }
 
     @Test
+    fun testFindFocus_noInputFocus() {
+        // Arrange.
+        setContent {
+            Row {
+                // No focused item.
+                Box(Modifier.size(10.dp).focusable())
+                Box(Modifier.size(10.dp).focusable())
+            }
+        }
+
+        // Act.
+        val focusedNode = rule.runOnUiThread { provider.findFocus(FOCUS_INPUT) }
+
+        // Assert.
+        assertThat(focusedNode).isNull()
+    }
+
+    @Test
+    fun testFindFocus_hasInputFocus() {
+        // Arrange.
+        val focusRequester = FocusRequester()
+        setContent {
+            Row {
+                // Initially focused item.
+                Box(Modifier.size(10.dp).focusable())
+                Box(Modifier.testTag(tag).focusRequester(focusRequester).focusable()) {
+                    BasicText("focusable")
+                }
+            }
+        }
+        rule.runOnIdle { focusRequester.requestFocus() }
+        val virtualViewId = rule.onNodeWithTag(tag).assert(expectValue(Focused, true)).semanticsId
+        val expectedNode = provider.createAccessibilityNodeInfo(virtualViewId)
+
+        // Act.
+        val actualNode = rule.runOnUiThread { provider.findFocus(FOCUS_INPUT) }
+
+        // Assert.
+        assertThat(actualNode).isEqualTo(expectedNode)
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Suppress("DEPRECATION")
     fun testAddExtraDataToAccessibilityNodeInfo_notMerged() {
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/layout/LookaheadDelegatesTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadDelegatesTest.kt
new file mode 100644
index 0000000..cb5de6d
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadDelegatesTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.ui.layout
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+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.movableContentOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LookaheadDelegatesTest {
+    @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+    @get:Rule val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
+
+    @Test
+    fun testResetLookaheadPassDelegate() {
+        var placeChild by mutableStateOf(true)
+        var useLookaheadScope by mutableStateOf(true)
+        rule.setContent {
+            val movableContent = remember {
+                movableContentOf {
+                    Row(Modifier.padding(5.dp).requiredSize(200.dp)) {
+                        Box(Modifier.size(100.dp))
+                        Box(Modifier.size(100.dp))
+                        if (!useLookaheadScope) {
+                            Box(Modifier.size(100.dp))
+                        }
+                    }
+                }
+            }
+            Box(
+                Modifier.layout { m, c ->
+                    m.measure(c).run {
+                        layout(width, height) {
+                            if (placeChild) {
+                                place(0, 0)
+                            }
+                        }
+                    }
+                }
+            ) {
+                // Move moveableContent from a parent in LookaheadScope to a parent that is not
+                // in a LookaheadScope.
+                if (useLookaheadScope) {
+                    Box { LookaheadScope { movableContent() } }
+                } else {
+                    movableContent()
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        placeChild = false
+        useLookaheadScope = !useLookaheadScope
+        rule.waitForIdle()
+
+        placeChild = true
+        rule.waitForIdle()
+    }
+}
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/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 811066f..54dec05 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -107,6 +107,8 @@
 import androidx.core.view.accessibility.AccessibilityEventCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.FOCUS_ACCESSIBILITY
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.FOCUS_INPUT
 import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
 import androidx.lifecycle.Lifecycle
 import kotlin.math.abs
@@ -342,7 +344,9 @@
     private val handler = Handler(Looper.getMainLooper())
     private var nodeProvider = ComposeAccessibilityNodeProvider()
 
+    private var accessibilityFocusedVirtualViewId = InvalidId
     private var focusedVirtualViewId = InvalidId
+    private var currentlyAccessibilityFocusedANI: AccessibilityNodeInfoCompat? = null
     private var currentlyFocusedANI: AccessibilityNodeInfoCompat? = null
     private var sendingFocusAffectingEvent = false
     private val pendingHorizontalScrollEvents = MutableIntObjectMap<ScrollAxisRange>()
@@ -624,7 +628,7 @@
         }
 
         // Manage internal accessibility focus state.
-        if (virtualViewId == focusedVirtualViewId) {
+        if (virtualViewId == accessibilityFocusedVirtualViewId) {
             info.isAccessibilityFocused = true
             info.addAction(AccessibilityActionCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS)
         } else {
@@ -703,6 +707,7 @@
             info.isFocused = semanticsNode.unmergedConfig[SemanticsProperties.Focused]
             if (info.isFocused) {
                 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS)
+                focusedVirtualViewId = virtualViewId
             } else {
                 info.addAction(AccessibilityNodeInfoCompat.ACTION_FOCUS)
             }
@@ -1104,7 +1109,7 @@
      * @return True if the view is accessibility focused.
      */
     private fun isAccessibilityFocused(virtualViewId: Int): Boolean {
-        return (focusedVirtualViewId == virtualViewId)
+        return (accessibilityFocusedVirtualViewId == virtualViewId)
     }
 
     /**
@@ -1125,15 +1130,15 @@
         // TODO: Check virtual view visibility.
         if (!isAccessibilityFocused(virtualViewId)) {
             // Clear focus from the previously focused view, if applicable.
-            if (focusedVirtualViewId != InvalidId) {
+            if (accessibilityFocusedVirtualViewId != InvalidId) {
                 sendEventForVirtualView(
-                    focusedVirtualViewId,
+                    accessibilityFocusedVirtualViewId,
                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
                 )
             }
 
             // Set focus on the new view.
-            focusedVirtualViewId = virtualViewId
+            accessibilityFocusedVirtualViewId = virtualViewId
             // TODO(b/272068594): Do we have to set currentlyFocusedANI object too?
 
             view.invalidate()
@@ -1265,8 +1270,8 @@
      */
     private fun clearAccessibilityFocus(virtualViewId: Int): Boolean {
         if (isAccessibilityFocused(virtualViewId)) {
-            focusedVirtualViewId = InvalidId
-            currentlyFocusedANI = null
+            accessibilityFocusedVirtualViewId = InvalidId
+            currentlyAccessibilityFocusedANI = null
             view.invalidate()
             sendEventForVirtualView(
                 virtualViewId,
@@ -2496,6 +2501,20 @@
 
                 // Refresh the current "green box" bounds and invalidate the View to tell
                 // ViewRootImpl to redraw it at its latest position.
+                currentSemanticsNodes[accessibilityFocusedVirtualViewId]?.let {
+                    try {
+                        currentlyAccessibilityFocusedANI?.setBoundsInScreen(boundsInScreen(it))
+                    } catch (e: IllegalStateException) {
+                        // setBoundsInScreen could in theory throw an IllegalStateException if the
+                        // system has previously sealed the AccessibilityNodeInfo.  This cannot
+                        // happen on stock AOSP, because ViewRootImpl only uses it for bounds
+                        // checking, and never forwards it to an accessibility service. But that is
+                        // a non-CTS-enforced implementation detail, so we should avoid crashing if
+                        // this happens.
+                    }
+                }
+                // Refresh the current "blue box" bounds and invalidate the View to tell
+                // ViewRootImpl to redraw it at its latest position.
                 currentSemanticsNodes[focusedVirtualViewId]?.let {
                     try {
                         currentlyFocusedANI?.setBoundsInScreen(boundsInScreen(it))
@@ -2820,8 +2839,13 @@
     private inner class ComposeAccessibilityNodeProvider : AccessibilityNodeProviderCompat() {
         override fun createAccessibilityNodeInfo(virtualViewId: Int): AccessibilityNodeInfoCompat? {
             return createNodeInfo(virtualViewId).also {
-                if (sendingFocusAffectingEvent && virtualViewId == focusedVirtualViewId) {
-                    currentlyFocusedANI = it
+                if (sendingFocusAffectingEvent) {
+                    if (virtualViewId == accessibilityFocusedVirtualViewId) {
+                        currentlyAccessibilityFocusedANI = it
+                    }
+                    if (virtualViewId == focusedVirtualViewId) {
+                        currentlyFocusedANI = it
+                    }
                 }
             }
         }
@@ -2840,7 +2864,15 @@
         }
 
         override fun findFocus(focus: Int): AccessibilityNodeInfoCompat? {
-            return createAccessibilityNodeInfo(focusedVirtualViewId)
+            return when (focus) {
+                // TODO(b/364744967): add test for  FOCUS_ACCESSIBILITY
+                FOCUS_ACCESSIBILITY ->
+                    createAccessibilityNodeInfo(accessibilityFocusedVirtualViewId)
+                FOCUS_INPUT ->
+                    if (focusedVirtualViewId == InvalidId) null
+                    else createAccessibilityNodeInfo(focusedVirtualViewId)
+                else -> throw IllegalArgumentException("Unknown focus type: $focus")
+            }
         }
     }
 
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/input/pointer/util/PointerIdArray.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/PointerIdArray.kt
index f295af3..d96d294 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/PointerIdArray.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/PointerIdArray.kt
@@ -19,7 +19,6 @@
 package androidx.compose.ui.input.pointer.util
 
 import androidx.compose.ui.input.pointer.PointerId
-import dalvik.annotation.optimization.NeverInline
 
 /**
  * This collection is specifically for dealing with [PointerId] values. We know that they contain
@@ -145,7 +144,6 @@
         if (index >= size) size = index + 1
     }
 
-    @NeverInline
     private fun resizeStorage(minSize: Int): LongArray {
         return internalArray.copyOf(maxOf(minSize, internalArray.size * 2)).apply {
             internalArray = this
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..1ce3b5b 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
@@ -118,6 +118,12 @@
                 if (newRoot != null) {
                     layoutDelegate.ensureLookaheadDelegateCreated()
                     forEachCoordinatorIncludingInner { it.ensureLookaheadDelegateCreated() }
+                } else {
+                    // When lookahead root is set to null, clear the lookahead pass delegate.
+                    // This can happen when lookaheadScope is removed in one of the parents, or
+                    // more likely when movableContent moves from a parent in a LookaheadScope to
+                    // a parent not in a LookaheadScope.
+                    layoutDelegate.clearLookaheadDelegate()
                 }
                 invalidateMeasurements()
             }
@@ -400,7 +406,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 +937,9 @@
             requirePrecondition(!isDeactivated) { "modifier is updated when deactivated" }
             if (isAttached) {
                 applyModifier(value)
+                if (isSemanticsInvalidated) {
+                    invalidateSemantics()
+                }
             } else {
                 pendingModifier = value
             }
@@ -935,19 +952,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/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index cb30258..65d8e99 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -368,6 +368,10 @@
         measurePassDelegate.childDelegatesDirty = true
         lookaheadPassDelegate?.let { it.childDelegatesDirty = true }
     }
+
+    fun clearLookaheadDelegate() {
+        lookaheadPassDelegate = null
+    }
 }
 
 /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MyersDiff.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MyersDiff.kt
index 1d84d69..acc7620 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MyersDiff.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MyersDiff.kt
@@ -18,7 +18,6 @@
 package androidx.compose.ui.node
 
 import androidx.compose.ui.internal.checkPrecondition
-import dalvik.annotation.optimization.NeverInline
 import kotlin.jvm.JvmInline
 import kotlin.math.abs
 import kotlin.math.min
@@ -418,7 +417,6 @@
     val size: Int
         get() = lastIndex
 
-    @NeverInline
     private fun resizeStack(stack: IntArray): IntArray {
         val copy = stack.copyOf(stack.size * 2)
         this.stack = copy
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/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt
index ab2eb07..259ae65 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+@file:Suppress("NOTHING_TO_INLINE")
 
 package androidx.compose.ui.spatial
 
-import dalvik.annotation.optimization.NeverInline
 import kotlin.jvm.JvmField
 import kotlin.math.max
 import kotlin.math.min
@@ -99,24 +98,19 @@
      * keep this in mind if you call this method and have cached any of those values in a local
      * variable, you may need to refresh them.
      */
-    private inline fun allocateItemsIndex(): Int {
+    internal fun allocateItemsIndex(): Int {
         val currentItems = items
         val currentSize = itemsSize
         itemsSize = currentSize + LongsPerItem
         val actualSize = currentItems.size
         if (actualSize <= currentSize + LongsPerItem) {
-            resizeStorage(actualSize, currentSize, currentItems)
+            val newSize = max(actualSize * 2, currentSize + LongsPerItem)
+            items = currentItems.copyOf(newSize)
+            stack = stack.copyOf(newSize)
         }
         return currentSize
     }
 
-    @NeverInline
-    private fun resizeStorage(actualSize: Int, currentSize: Int, currentItems: LongArray) {
-        val newSize = max(actualSize * 2, currentSize + LongsPerItem)
-        items = currentItems.copyOf(newSize)
-        stack = stack.copyOf(newSize)
-    }
-
     /**
      * Insert a value and corresponding bounding rectangle into the RectList. This method does not
      * check to see that [value] doesn't already exist somewhere in the list.
diff --git a/constraintlayout/constraintlayout-compose/api/current.ignore b/constraintlayout/constraintlayout-compose/api/current.ignore
new file mode 100644
index 0000000..dfb0518a
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/api/current.ignore
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.constraintlayout.compose.DebugFlags#getShowBounds():
+    Removed method androidx.constraintlayout.compose.DebugFlags.getShowBounds()
+RemovedMethod: androidx.constraintlayout.compose.DebugFlags#getShowKeyPositions():
+    Removed method androidx.constraintlayout.compose.DebugFlags.getShowKeyPositions()
+RemovedMethod: androidx.constraintlayout.compose.DebugFlags#getShowPaths():
+    Removed method androidx.constraintlayout.compose.DebugFlags.getShowPaths()
+RemovedMethod: androidx.constraintlayout.compose.GridFlag#isPlaceLayoutsOnSpansFirst():
+    Removed method androidx.constraintlayout.compose.GridFlag.isPlaceLayoutsOnSpansFirst()
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.ignore b/constraintlayout/constraintlayout-compose/api/restricted_current.ignore
new file mode 100644
index 0000000..dfb0518a
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.ignore
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.constraintlayout.compose.DebugFlags#getShowBounds():
+    Removed method androidx.constraintlayout.compose.DebugFlags.getShowBounds()
+RemovedMethod: androidx.constraintlayout.compose.DebugFlags#getShowKeyPositions():
+    Removed method androidx.constraintlayout.compose.DebugFlags.getShowKeyPositions()
+RemovedMethod: androidx.constraintlayout.compose.DebugFlags#getShowPaths():
+    Removed method androidx.constraintlayout.compose.DebugFlags.getShowPaths()
+RemovedMethod: androidx.constraintlayout.compose.GridFlag#isPlaceLayoutsOnSpansFirst():
+    Removed method androidx.constraintlayout.compose.GridFlag.isPlaceLayoutsOnSpansFirst()
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index 467bc76..339b479 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -679,8 +679,7 @@
     method public final void drawDebugBounds(androidx.compose.ui.graphics.drawscope.DrawScope, float forcedScaleFactor);
     method public String getDesignInfo(int startX, int startY, String args);
     method public final float getForcedScaleFactor();
-    method @Deprecated protected final java.util.Map<androidx.compose.ui.layout.Measurable,androidx.constraintlayout.core.state.WidgetFrame> getFrameCache();
-    method protected final java.util.Map<java.lang.String,androidx.constraintlayout.core.state.WidgetFrame> getFrameCache2();
+    method protected final java.util.Map<androidx.compose.ui.layout.Measurable,androidx.constraintlayout.core.state.WidgetFrame> getFrameCache();
     method public final int getLayoutCurrentHeight();
     method public final int getLayoutCurrentWidth();
     method protected final androidx.constraintlayout.compose.LayoutInformationReceiver? getLayoutInformationReceiver();
@@ -689,16 +688,12 @@
     method protected final androidx.constraintlayout.compose.State getState();
     method public void measure(androidx.constraintlayout.core.widgets.ConstraintWidget constraintWidget, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure measure);
     method public final void parseDesignElements(androidx.constraintlayout.compose.ConstraintSet constraintSet);
-    method @Deprecated public final void performLayout(androidx.compose.ui.layout.Placeable.PlacementScope, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
-    method public final void performLayout(androidx.compose.ui.layout.Placeable.PlacementScope, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, java.util.Map<androidx.compose.ui.layout.Measurable,androidx.compose.ui.layout.Placeable> placeableMap);
-    method @Deprecated public final long performMeasure(long constraints, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.constraintlayout.compose.ConstraintSet constraintSet, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, int optimizationLevel);
-    method public final long performMeasure(long constraints, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.constraintlayout.compose.ConstraintSet constraintSet, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, java.util.Map<androidx.compose.ui.layout.Measurable,androidx.compose.ui.layout.Placeable> placeableMap, int optimizationLevel);
+    method public final void performLayout(androidx.compose.ui.layout.Placeable.PlacementScope, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+    method public final long performMeasure(long constraints, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.constraintlayout.compose.ConstraintSet constraintSet, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, int optimizationLevel);
     method public final void setForcedScaleFactor(float);
     method protected final void setLayoutInformationReceiver(androidx.constraintlayout.compose.LayoutInformationReceiver?);
-    method protected final void setPlaceables(java.util.Map<androidx.compose.ui.layout.Measurable,androidx.compose.ui.layout.Placeable>);
     property public final float forcedScaleFactor;
-    property @Deprecated protected final java.util.Map<androidx.compose.ui.layout.Measurable,androidx.constraintlayout.core.state.WidgetFrame> frameCache;
-    property protected final java.util.Map<java.lang.String,androidx.constraintlayout.core.state.WidgetFrame> frameCache2;
+    property protected final java.util.Map<androidx.compose.ui.layout.Measurable,androidx.constraintlayout.core.state.WidgetFrame> frameCache;
     property public final int layoutCurrentHeight;
     property public final int layoutCurrentWidth;
     property protected final androidx.constraintlayout.compose.LayoutInformationReceiver? layoutInformationReceiver;
diff --git a/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
index c6ccca1..e7f7289 100644
--- a/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
@@ -44,7 +44,6 @@
 import androidx.compose.ui.graphics.asAndroidBitmap
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.FirstBaseline
-import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.boundsInParent
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.layoutId
@@ -2455,138 +2454,6 @@
             assertEquals(IntSize(rootSizePx.fastRoundToInt(), 0), layoutSize)
         }
 
-    @Test
-    fun testToggleVisibilityWithFillConstraintsWidth() =
-        with(rule.density) {
-            val rootSizePx = 100f
-
-            var toggleVisibility by mutableStateOf(false)
-
-            rule.setContent {
-                // Regression test, modify only dimensions if necessary
-                ConstraintLayout(modifier = Modifier.size(rootSizePx.toDp())) {
-                    val (titleRef, detailRef) = createRefs()
-                    Box(
-                        modifier =
-                            Modifier.background(Color.Cyan).testTag("box1").constrainAs(detailRef) {
-                                centerHorizontallyTo(parent)
-
-                                width = Dimension.fillToConstraints
-                                height = rootSizePx.toDp().asDimension()
-
-                                visibility =
-                                    if (!toggleVisibility) Visibility.Gone else Visibility.Visible
-                            }
-                    )
-                    Box(
-                        modifier =
-                            Modifier.background(Color.Red).testTag("box2").constrainAs(titleRef) {
-                                centerHorizontallyTo(parent)
-
-                                width = Dimension.fillToConstraints
-                                height = rootSizePx.toDp().asDimension()
-
-                                visibility =
-                                    if (toggleVisibility) Visibility.Gone else Visibility.Visible
-                            }
-                    )
-                }
-            }
-            rule.waitForIdle()
-
-            rule.onNodeWithTag("box1").apply {
-                assertWidthIsEqualTo(Dp.Unspecified)
-                assertHeightIsEqualTo(Dp.Unspecified)
-            }
-            rule.onNodeWithTag("box2").apply {
-                assertWidthIsEqualTo(rootSizePx.toDp())
-                assertHeightIsEqualTo(rootSizePx.toDp())
-            }
-
-            toggleVisibility = !toggleVisibility
-            rule.waitForIdle()
-
-            rule.onNodeWithTag("box1").apply {
-                assertWidthIsEqualTo(rootSizePx.toDp())
-                assertHeightIsEqualTo(rootSizePx.toDp())
-            }
-            rule.onNodeWithTag("box2").apply {
-                assertWidthIsEqualTo(Dp.Unspecified)
-                assertHeightIsEqualTo(Dp.Unspecified)
-            }
-            Unit // Test expects to return Unit
-        }
-
-    @Test
-    fun testToggleVisibilityWithFillConstraintsWidth_underLookahead() =
-        with(rule.density) {
-            val rootSizePx = 100f
-
-            var toggleVisibility by mutableStateOf(false)
-
-            rule.setContent {
-                LookaheadScope {
-                    // Regression test, modify only dimensions if necessary
-                    ConstraintLayout(modifier = Modifier.size(rootSizePx.toDp())) {
-                        val (titleRef, detailRef) = createRefs()
-                        Box(
-                            modifier =
-                                Modifier.background(Color.Cyan).testTag("box1").constrainAs(
-                                    detailRef
-                                ) {
-                                    centerHorizontallyTo(parent)
-
-                                    width = Dimension.fillToConstraints
-                                    height = rootSizePx.toDp().asDimension()
-
-                                    visibility =
-                                        if (!toggleVisibility) Visibility.Gone
-                                        else Visibility.Visible
-                                }
-                        )
-                        Box(
-                            modifier =
-                                Modifier.background(Color.Red).testTag("box2").constrainAs(
-                                    titleRef
-                                ) {
-                                    centerHorizontallyTo(parent)
-
-                                    width = Dimension.fillToConstraints
-                                    height = rootSizePx.toDp().asDimension()
-
-                                    visibility =
-                                        if (toggleVisibility) Visibility.Gone
-                                        else Visibility.Visible
-                                }
-                        )
-                    }
-                }
-            }
-            rule.waitForIdle()
-
-            rule.onNodeWithTag("box1").apply {
-                assertWidthIsEqualTo(Dp.Unspecified)
-                assertHeightIsEqualTo(Dp.Unspecified)
-            }
-            rule.onNodeWithTag("box2").apply {
-                assertWidthIsEqualTo(rootSizePx.toDp())
-                assertHeightIsEqualTo(rootSizePx.toDp())
-            }
-
-            toggleVisibility = !toggleVisibility
-            rule.waitForIdle()
-
-            rule.onNodeWithTag("box1").apply {
-                assertWidthIsEqualTo(rootSizePx.toDp())
-                assertHeightIsEqualTo(rootSizePx.toDp())
-            }
-            rule.onNodeWithTag("box2").apply {
-                assertWidthIsEqualTo(Dp.Unspecified)
-                assertHeightIsEqualTo(Dp.Unspecified)
-            }
-            Unit // Test expects to return Unit
-        }
-
     /**
      * Provides a list constraints combination for horizontal anchors: `start`, `end`,
      * `absoluteLeft`, `absoluteRight`.
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
index da91f65..04acd28 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
@@ -420,27 +420,21 @@
     val contentTracker = remember { mutableStateOf(Unit, neverEqualPolicy()) }
 
     val measurePolicy = MeasurePolicy { measurables, constraints ->
-        // Map to properly capture Placeables across Measure and Layout passes
-        val placeableMap = mutableMapOf<Measurable, Placeable>()
-
-        // Call to invalidate measure on content recomposition
         contentTracker.value
-
         val layoutSize =
             measurer.performMeasure(
-                constraints = constraints,
-                layoutDirection = layoutDirection,
-                constraintSet = constraintSet,
-                measurables = measurables,
-                placeableMap = placeableMap,
-                optimizationLevel = optimizationLevel
+                constraints,
+                layoutDirection,
+                constraintSet,
+                measurables,
+                optimizationLevel
             )
         // We read the remeasurement requester state, to request remeasure when the value
         // changes. This will happen when the scope helpers are changing at recomposition.
         remeasureRequesterState.value
 
         layout(layoutSize.width, layoutSize.height) {
-            with(measurer) { performLayout(measurables = measurables, placeableMap = placeableMap) }
+            with(measurer) { performLayout(measurables) }
         }
     }
 
@@ -811,25 +805,17 @@
             true
         }
         val measurePolicy = MeasurePolicy { measurables, constraints ->
-            // Map to properly capture Placeables across Measure and Layout passes
-            val placeableMap = mutableMapOf<Measurable, Placeable>()
-
-            // Call to invalidate measure on content recomposition
             contentTracker.value
-
             val layoutSize =
                 measurer.performMeasure(
-                    constraints = constraints,
-                    layoutDirection = layoutDirection,
-                    constraintSet = constraintSet,
-                    measurables = measurables,
-                    placeableMap = placeableMap,
-                    optimizationLevel = optimizationLevel
+                    constraints,
+                    layoutDirection,
+                    constraintSet,
+                    measurables,
+                    optimizationLevel
                 )
             layout(layoutSize.width, layoutSize.height) {
-                with(measurer) {
-                    performLayout(measurables = measurables, placeableMap = placeableMap)
-                }
+                with(measurer) { performLayout(measurables) }
             }
         }
         if (constraintSet is EditableJSONLayout) {
@@ -1625,22 +1611,9 @@
     private var computedLayoutResult: String = ""
     protected var layoutInformationReceiver: LayoutInformationReceiver? = null
     protected val root = ConstraintWidgetContainer(0, 0).also { it.measurer = this }
-
-    /**
-     * Due to Lookahead measure pass, the object used to measure and place should not be
-     * instantiated internally, instead, should be instantiated within the MeasurePolicy call, and
-     * then passed to update this variable, at each the measure and layout passes.
-     */
-    protected var placeables = mutableMapOf<Measurable, Placeable>()
+    protected val placeables = mutableMapOf<Measurable, Placeable>()
     private val lastMeasures = mutableMapOf<String, Array<Int>>()
-
-    @Suppress("unused") // Exists for compatibility.
-    @Deprecated(
-        message = "Should not reference a Measurable.",
-        replaceWith = ReplaceWith("frameCache2")
-    )
-    protected val frameCache = emptyMap<Measurable, WidgetFrame>()
-    protected val frameCache2 = mutableMapOf<String, WidgetFrame>()
+    protected val frameCache = mutableMapOf<Measurable, WidgetFrame>()
 
     protected val state = State(density)
 
@@ -1817,7 +1790,7 @@
                 val id = measurable.layoutId ?: measurable.constraintLayoutId
                 child.stringId = id?.toString()
             }
-            val frame = frameCache2[measurable.anyOrNullId]?.widget?.frame
+            val frame = frameCache[measurable]?.widget?.frame
             if (frame == null) {
                 continue
             }
@@ -1894,33 +1867,13 @@
         this[2] = measure.measuredBaseline
     }
 
-    @Suppress("DeprecatedCallableAddReplaceWith") // Requires manual replacement
-    @Deprecated("Should receive placeable Map from caller.")
     fun performMeasure(
         constraints: Constraints,
         layoutDirection: LayoutDirection,
         constraintSet: ConstraintSet,
         measurables: List<Measurable>,
         optimizationLevel: Int
-    ): IntSize =
-        performMeasure(
-            constraints = constraints,
-            layoutDirection = layoutDirection,
-            constraintSet = constraintSet,
-            measurables = measurables,
-            placeableMap = placeables,
-            optimizationLevel = optimizationLevel
-        )
-
-    fun performMeasure(
-        constraints: Constraints,
-        layoutDirection: LayoutDirection,
-        constraintSet: ConstraintSet,
-        measurables: List<Measurable>,
-        placeableMap: MutableMap<Measurable, Placeable>,
-        optimizationLevel: Int
     ): IntSize {
-        this.placeables = placeableMap
         if (measurables.isEmpty()) {
             // TODO(b/335524398): Behavior with zero children is unexpected. It's also inconsistent
             //      with ViewGroup, so this is a workaround to handle those cases the way it seems
@@ -1985,7 +1938,7 @@
     internal fun resetMeasureState() {
         placeables.clear()
         lastMeasures.clear()
-        frameCache2.clear()
+        frameCache.clear()
     }
 
     protected fun applyRootSize(constraints: Constraints) {
@@ -2024,30 +1977,37 @@
         }
     }
 
-    @Suppress("DeprecatedCallableAddReplaceWith") // Requires manual replacement
-    @Deprecated("Should receive placeable Map from caller.")
     fun Placeable.PlacementScope.performLayout(measurables: List<Measurable>) {
-        performLayout(measurables, placeables)
-    }
-
-    fun Placeable.PlacementScope.performLayout(
-        measurables: List<Measurable>,
-        placeableMap: MutableMap<Measurable, Placeable>,
-    ) {
-        if (frameCache2.isEmpty()) {
+        if (frameCache.isEmpty()) {
             root.children.fastForEach { child ->
                 val measurable = child.companionWidget
                 if (measurable !is Measurable) return@fastForEach
                 val frame = WidgetFrame(child.frame.update())
-                frameCache2[measurable.anyOrNullId] = frame
+                frameCache[measurable] = frame
             }
         }
         measurables.fastForEach { measurable ->
-            val frame = frameCache2[measurable.anyOrNullId] ?: return@fastForEach
-            // Don't use `placeables` map from Measurer, is not guaranteed to correspond to this
-            // Layout pass
-            val placeable = placeableMap[measurable] ?: return@fastForEach
-            placeWithFrameTransform(placeable, frame)
+            val matchedMeasurable: Measurable =
+                if (!frameCache.containsKey(measurable)) {
+                    // TODO: Workaround for lookaheadLayout, the measurable is a different instance
+                    frameCache.keys.firstOrNull {
+                        it.layoutId != null && it.layoutId == measurable.layoutId
+                    } ?: return@fastForEach
+                } else {
+                    measurable
+                }
+            val frame = frameCache[matchedMeasurable] ?: return
+            val placeable = placeables[matchedMeasurable] ?: return
+            if (!frameCache.containsKey(measurable)) {
+                // TODO: Workaround for lookaheadLayout, the measurable is a different instance and
+                //   the placeable should be a result of the given measurable
+                placeWithFrameTransform(
+                    measurable.measure(Constraints.fixed(placeable.width, placeable.height)),
+                    frame
+                )
+            } else {
+                placeWithFrameTransform(placeable, frame)
+            }
         }
         if (layoutInformationReceiver?.getLayoutInformationMode() == LayoutInfoFlags.BOUNDS) {
             computeLayoutResult()
@@ -2096,8 +2056,7 @@
                 IntIntPair(constraintWidget.measuredWidth, constraintWidget.measuredHeight)
             }
             measurable is Measurable -> {
-                val result = measurable.measure(constraints)
-                placeables[measurable] = result
+                val result = measurable.measure(constraints).also { placeables[measurable] = it }
                 IntIntPair(result.width, result.height)
             }
             else -> {
@@ -2316,10 +2275,6 @@
     }
 }
 
-/** Returns either [LayoutIdParentData] or [ConstraintLayoutParentData] id. Otherwise "null". */
-internal val Measurable.anyOrNullId: String
-    get() = (this.layoutId ?: this.constraintLayoutId)?.toString() ?: "null"
-
 internal typealias SolverDimension = androidx.constraintlayout.core.state.Dimension
 
 internal typealias SolverState = androidx.constraintlayout.core.state.State
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/LateMotionLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/LateMotionLayout.kt
index 46b7f27..8287e20 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/LateMotionLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/LateMotionLayout.kt
@@ -25,10 +25,8 @@
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.layout.MultiMeasureLayout
-import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.node.Ref
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.semantics.semantics
@@ -119,9 +117,6 @@
     measurer: MotionMeasurer,
     optimizationLevel: Int,
 ): MeasurePolicy = MeasurePolicy { measurables, constraints ->
-    // Map to properly capture Placeables across Measure and Layout passes
-    val placeableMap = mutableMapOf<Measurable, Placeable>()
-
     // Do a state read, to guarantee that we control measure when the content recomposes without
     // notifying our Composable caller
     contentTracker.value
@@ -134,7 +129,6 @@
             constraintSetEnd = endProvider(),
             transition = TransitionImpl.EMPTY,
             measurables = measurables,
-            placeableMap = placeableMap,
             optimizationLevel = optimizationLevel,
             progress = motionProgress.value,
             compositionSource = compositionSource.value ?: CompositionSource.Unknown,
@@ -142,7 +136,5 @@
         )
     compositionSource.value = CompositionSource.Unknown // Reset after measuring
 
-    layout(layoutSize.width, layoutSize.height) {
-        with(measurer) { performLayout(measurables = measurables, placeableMap = placeableMap) }
-    }
+    layout(layoutSize.width, layoutSize.height) { with(measurer) { performLayout(measurables) } }
 }
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-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
index 8b0e892..280dca8 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
@@ -42,10 +42,8 @@
 import androidx.compose.ui.draw.scale
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.layout.MultiMeasureLayout
-import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.node.Ref
@@ -980,9 +978,6 @@
     optimizationLevel: Int,
     invalidationStrategy: InvalidationStrategy
 ): MeasurePolicy = MeasurePolicy { measurables, constraints ->
-    // Map to properly capture Placeables across Measure and Layout passes
-    val placeableMap = mutableMapOf<Measurable, Placeable>()
-
     // Do a state read, to guarantee that we control measure when the content recomposes without
     // notifying our Composable caller
     contentTracker.value
@@ -995,7 +990,6 @@
             constraintSetEnd = constraintSetEnd,
             transition = transition,
             measurables = measurables,
-            placeableMap = placeableMap,
             optimizationLevel = optimizationLevel,
             progress = motionProgress.floatValue,
             compositionSource = compositionSource.value ?: CompositionSource.Unknown,
@@ -1003,9 +997,7 @@
         )
     compositionSource.value = CompositionSource.Unknown // Reset after measuring
 
-    layout(layoutSize.width, layoutSize.height) {
-        with(measurer) { performLayout(measurables = measurables, placeableMap = placeableMap) }
-    }
+    layout(layoutSize.width, layoutSize.height) { with(measurer) { performLayout(measurables) } }
 }
 
 /**
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
index b077a37..ecd807c 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
@@ -27,7 +27,6 @@
 import androidx.compose.ui.graphics.drawscope.translate
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
@@ -85,13 +84,11 @@
         constraintSetEnd: ConstraintSet,
         @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl,
         measurables: List<Measurable>,
-        placeableMap: MutableMap<Measurable, Placeable>,
         optimizationLevel: Int,
         progress: Float,
         compositionSource: CompositionSource,
         invalidateOnConstraintsCallback: ShouldInvalidateCallback?
     ): IntSize {
-        placeables = placeableMap
         val needsRemeasure =
             needsRemeasure(
                 constraints = constraints,
@@ -142,7 +139,7 @@
         source: CompositionSource,
         invalidateOnConstraintsCallback: ShouldInvalidateCallback?
     ): Boolean {
-        if (this.transition.isEmpty || frameCache2.isEmpty()) {
+        if (this.transition.isEmpty || frameCache.isEmpty()) {
             // Nothing measured (by MotionMeasurer)
             return true
         }
@@ -229,7 +226,7 @@
                 measurable.measure(
                     Constraints.fixed(interpolatedFrame.width(), interpolatedFrame.height())
                 )
-            frameCache2[measurable.anyOrNullId] = interpolatedFrame
+            frameCache[measurable] = interpolatedFrame
         }
 
         if (layoutInformationReceiver?.getLayoutInformationMode() == LayoutInfoFlags.BOUNDS) {
@@ -542,7 +539,7 @@
 
     fun clearConstraintSets() {
         transition.clear()
-        frameCache2.clear()
+        frameCache.clear()
     }
 
     @Suppress("UnavailableSymbol")
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/contentpager/contentpager/build.gradle b/contentpager/contentpager/build.gradle
index 4d04d51..b9a4313 100644
--- a/contentpager/contentpager/build.gradle
+++ b/contentpager/contentpager/build.gradle
@@ -29,6 +29,7 @@
 }
 
 dependencies {
+    api(libs.jspecify)
     api("androidx.annotation:annotation:1.8.1")
     api("androidx.core:core:1.1.0")
     implementation("androidx.collection:collection:1.4.2")
@@ -47,8 +48,6 @@
     inceptionYear = "2017"
     description = "Library providing support for paging across content exposed via a ContentProvider. Use of this library allows a client to avoid expensive interprocess \"cursor window swaps\" on the UI thread."
     failOnDeprecationWarnings = false
-    // TODO: b/326456246
-    optOutJSpecify = true
 }
 
 android {
diff --git a/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/ContentPagerTest.java b/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/ContentPagerTest.java
index 7b5f3c7..950e8eb 100644
--- a/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/ContentPagerTest.java
+++ b/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/ContentPagerTest.java
@@ -33,11 +33,11 @@
 import android.os.Handler;
 import android.os.Looper;
 
-import androidx.annotation.Nullable;
 import androidx.contentpager.content.ContentPager.ContentCallback;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import org.jspecify.annotations.Nullable;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/LoaderQueryRunnerTest.java b/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/LoaderQueryRunnerTest.java
index a6eaaa8..6f4f4e0 100644
--- a/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/LoaderQueryRunnerTest.java
+++ b/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/LoaderQueryRunnerTest.java
@@ -22,12 +22,12 @@
 import android.app.Activity;
 import android.database.Cursor;
 
-import androidx.annotation.NonNull;
 import androidx.contentpager.content.ContentPager.ContentCallback;
 import androidx.contentpager.content.ContentPager.QueryRunner;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 
+import org.jspecify.annotations.NonNull;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/TestContentProvider.java b/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/TestContentProvider.java
index 885bd1e..88eeed4 100644
--- a/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/TestContentProvider.java
+++ b/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/TestContentProvider.java
@@ -30,9 +30,10 @@
 import android.os.Bundle;
 import android.os.CancellationSignal;
 
-import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import org.jspecify.annotations.Nullable;
+
 /**
  * A stub data paging provider used for testing of paging support.
  * Ignores client supplied projections.
@@ -89,7 +90,7 @@
 
     @Override
     public Cursor query(
-            Uri uri, @Nullable String[] projection, String selection, String[] selectionArgs,
+            Uri uri, String @Nullable [] projection, String selection, String[] selectionArgs,
             String sortOrder) {
         return query(uri, projection, null, null);
     }
@@ -134,7 +135,7 @@
             return value;
         }
 
-        @Nullable String argValue = uri.getQueryParameter(key);
+        String argValue = uri.getQueryParameter(key);
         if (argValue != null) {
             try {
                 return Integer.parseInt(argValue);
diff --git a/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/TestQueryCallback.java b/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/TestQueryCallback.java
index 320d463..737ae17 100644
--- a/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/TestQueryCallback.java
+++ b/contentpager/contentpager/src/androidTest/java/androidx/contentpager/content/TestQueryCallback.java
@@ -25,9 +25,10 @@
 import android.net.Uri;
 import android.os.Bundle;
 
-import androidx.annotation.Nullable;
 import androidx.core.util.Pair;
 
+import org.jspecify.annotations.Nullable;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
diff --git a/contentpager/contentpager/src/main/java/androidx/contentpager/content/ContentPager.java b/contentpager/contentpager/src/main/java/androidx/contentpager/content/ContentPager.java
index 2882853..2f5fbac 100644
--- a/contentpager/contentpager/src/main/java/androidx/contentpager/content/ContentPager.java
+++ b/contentpager/contentpager/src/main/java/androidx/contentpager/content/ContentPager.java
@@ -34,13 +34,14 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.IntDef;
 import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.RequiresPermission;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 import androidx.collection.LruCache;
 
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.HashSet;
@@ -279,8 +280,8 @@
      */
     @MainThread
     public @NonNull Query query(
-            @NonNull @RequiresPermission.Read Uri uri,
-            @Nullable String[] projection,
+            @RequiresPermission.Read @NonNull Uri uri,
+            String @Nullable [] projection,
             @NonNull Bundle queryArgs,
             @Nullable CancellationSignal cancellationSignal,
             @NonNull ContentCallback callback) {
diff --git a/contentpager/contentpager/src/main/java/androidx/contentpager/content/LoaderQueryRunner.java b/contentpager/contentpager/src/main/java/androidx/contentpager/content/LoaderQueryRunner.java
index 7054cfe..dbcbb05 100644
--- a/contentpager/contentpager/src/main/java/androidx/contentpager/content/LoaderQueryRunner.java
+++ b/contentpager/contentpager/src/main/java/androidx/contentpager/content/LoaderQueryRunner.java
@@ -25,7 +25,7 @@
 import android.os.Bundle;
 import android.util.Log;
 
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
 
 /**
  * A {@link ContentPager.QueryRunner} that executes queries using a {@link LoaderManager}.
@@ -48,7 +48,7 @@
 
     @Override
     @SuppressWarnings({"unchecked", "deprecation"})
-    public void query(final @NonNull Query query, @NonNull final Callback callback) {
+    public void query(final @NonNull Query query, final @NonNull Callback callback) {
         if (DEBUG) Log.d(TAG, "Handling query: " + query);
 
         android.app.LoaderManager.LoaderCallbacks<Cursor> callbacks =
diff --git a/contentpager/contentpager/src/main/java/androidx/contentpager/content/Query.java b/contentpager/contentpager/src/main/java/androidx/contentpager/content/Query.java
index a48c380..4c6d9fb 100644
--- a/contentpager/contentpager/src/main/java/androidx/contentpager/content/Query.java
+++ b/contentpager/contentpager/src/main/java/androidx/contentpager/content/Query.java
@@ -26,8 +26,8 @@
 import android.os.CancellationSignal;
 import android.util.Log;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import java.util.Arrays;
 
@@ -41,7 +41,7 @@
     private static final String TAG = "Query";
 
     private final Uri mUri;
-    private final @Nullable String[] mProjection;
+    private final String @Nullable [] mProjection;
     private final Bundle mQueryArgs;
 
     private final int mId;
@@ -53,10 +53,10 @@
 
     Query(
             @NonNull Uri uri,
-            @Nullable String[] projection,
+            String @Nullable [] projection,
             @NonNull Bundle args,
             @Nullable CancellationSignal cancellationSignal,
-            @NonNull ContentPager.ContentCallback callback) {
+            ContentPager.@NonNull ContentCallback callback) {
 
         checkArgument(uri != null);
         checkArgument(args != null);
@@ -107,7 +107,7 @@
         return mLimit;
     }
 
-    @NonNull ContentPager.ContentCallback getCallback() {
+    ContentPager.@NonNull ContentCallback getCallback() {
         return mCallback;
     }
 
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/PreCallEndpointsTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/PreCallEndpointsTest.kt
index f1bcaff..196e174 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/PreCallEndpointsTest.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/PreCallEndpointsTest.kt
@@ -112,4 +112,27 @@
         val res = currentPreCallEndpoints.maybeRemoveCallEndpoint(defaultSpeaker)
         assertEquals(PreCallEndpointsUpdater.STOP_TRACKING_REMOVED_ENDPOINT, res)
     }
+
+    /**
+     * This test verifies that the updateClient() function returns an immutable list. It checks that
+     * attempting to modify the returned list (using reversed()) does not alter its contents.
+     */
+    @SmallTest
+    @Test
+    fun testPreCallEndpointUpdaterEmitsImmutableList() {
+        // Given: a PreCallEndpointsUpdater
+        val sendChannel = Channel<List<CallEndpointCompat>>(Channel.BUFFERED)
+        val currentPreCallEndpoints =
+            PreCallEndpointsUpdater(mutableListOf(defaultEarpiece, defaultSpeaker), sendChannel)
+        // When: an update is emitted to the client
+        val finalList = currentPreCallEndpoints.updateClient()
+        assertEquals(defaultSpeaker, finalList[0])
+        assertEquals(defaultEarpiece, finalList[1])
+        // Then: verify the list is immutable
+        finalList.reversed()
+        assertEquals(defaultSpeaker, finalList[0])
+        assertEquals(defaultEarpiece, finalList[1])
+        // cleanup
+        sendChannel.close()
+    }
 }
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/PreCallEndpointsUpdater.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/PreCallEndpointsUpdater.kt
index 837d51c..b949f00 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/PreCallEndpointsUpdater.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/PreCallEndpointsUpdater.kt
@@ -128,9 +128,7 @@
         }
     }
 
-    private fun updateClient() {
-        // Sort by endpoint type.  The first element has the highest priority!
-        mCurrentDevices.sort()
-        mSendChannel.trySend(mCurrentDevices)
+    internal fun updateClient(): List<CallEndpointCompat> {
+        return mCurrentDevices.sorted().also { mSendChannel.trySend(it) }
     }
 }
diff --git a/development/requirerelnote.py b/development/requirerelnote.py
new file mode 100755
index 0000000..2f734d3
--- /dev/null
+++ b/development/requirerelnote.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+
+#
+# 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.
+#
+
+"""Script that enforces Relnote: any file path in the commit contains module substring"""
+
+import argparse
+import os.path
+import re
+import sys
+
+ERROR_RELNOTE_REQUIRED = """
+RELNOTE: is required for commits that contain changes in {}
+
+Please add a RELNOTE to the commit or RELNOTE: N/A if a release note is not applicable to the
+commit.
+
+A RELNOTE is required for all commits that changes the release artifacts.
+
+A RELNOTE can be N/A for commit messages that only effects tooling, documentation, directory
+structure, etc., but not the release artifacts.
+"""
+
+def main(args=None):
+    parser = argparse.ArgumentParser(
+        prog="requirerelnote",
+        description="Check if RELNOTE is required")
+    parser.add_argument('--file', nargs='+')
+    parser.add_argument('--module')
+    parser.add_argument('--commit')
+
+    args = parser.parse_args()
+
+    source_files = [f for f in args.file
+               if (not "buildSrc/" in f and
+                  "/src/main/" in f or
+                  "/src/commonMain/" in f or
+                  "/src/androidMain/" in f)]
+    module_files = [f for f in source_files
+                if (args.module in f)]
+
+    if not module_files:
+        sys.exit(0)
+
+    """Following copied (with minor edits) from hooks.py:check_commit_msg_relnote_for_current_txt"""
+    """Check if the commit contain the 'Relnote:' stanza."""
+    field = 'Relnote'
+    regex = fr'^{field}: .+$'
+    check_re = re.compile(regex, re.IGNORECASE)
+
+    found = []
+    for line in args.commit.splitlines():
+        if check_re.match(line):
+            found.append(line)
+
+    if not found:
+        print(ERROR_RELNOTE_REQUIRED.format(args.module))
+        sys.exit(1)
+
+    sys.exit(0)
+
+if __name__ == '__main__':
+  main()
\ No newline at end of file
diff --git a/docs/testing.md b/docs/testing.md
index b5b031f..95ae6b4 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -27,14 +27,13 @@
 pre-release API level.
 
 In practice, this is limited by device and emulator availability and
-reliability. As of November 2023, we run tests on the following API levels:
+reliability. As of January 2025, we run tests on the following API levels:
 
 -   API level 21: the lowest API level supported by Firebase Test Lab (FTL)
 -   API level 26: the lowest supported ARM-based emulator FTL runner, which has
     much greater performance and stability
--   API level 28: provides coverage between 26 and 30
--   API levels 30, 31, 33: the latest supported API levels, which represent the
-    majority of devices in the field
+-   API levels 30, 33, 34, 35: the latest supported API levels, which represent
+    the majority of devices in the field
 
 ## Adding tests {#adding}
 
@@ -433,9 +432,10 @@
 # Run instrumentation tests in Firebase Test Lab (remote)
 ./gradlew <project-name>:ftlnexus4api21
 ./gradlew <project-name>:ftlpixel2api26
-./gradlew <project-name>:ftlpixel2api28
 ./gradlew <project-name>:ftlpixel2api30
 ./gradlew <project-name>:ftlpixel2api33
+./gradlew <project-name>:ftlmediumphoneapi34
+./gradlew <project-name>:ftlmediumphoneapi35
 
 # Run local unit tests
 ./gradlew <project-name>:test
diff --git a/inspection/inspection/build.gradle b/inspection/inspection/build.gradle
index af4ba30..b721a23 100644
--- a/inspection/inspection/build.gradle
+++ b/inspection/inspection/build.gradle
@@ -30,6 +30,7 @@
 }
 
 dependencies {
+    api(libs.jspecify)
     api("androidx.annotation:annotation:1.8.1")
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.testCore)
@@ -46,8 +47,6 @@
     description = "Experimental AndroidX Inspection Project"
     legacyDisableKotlinStrictApiMode = true
     doNotDocumentReason = "Not shipped externally"
-    // TODO: b/326456246
-    optOutJSpecify = true
 }
 
 android {
diff --git a/inspection/inspection/src/main/java/androidx/inspection/ArtTooling.java b/inspection/inspection/src/main/java/androidx/inspection/ArtTooling.java
index 3727a8b..03ca297 100644
--- a/inspection/inspection/src/main/java/androidx/inspection/ArtTooling.java
+++ b/inspection/inspection/src/main/java/androidx/inspection/ArtTooling.java
@@ -16,8 +16,8 @@
 
 package androidx.inspection;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import java.util.List;
 
@@ -33,8 +33,7 @@
      * @param clazz class whose instances should be looked up
      * @return a list of instances of {@code clazz}
      */
-    @NonNull
-    <T> List<T> findInstances(@NonNull Class<T> clazz);
+    <T> @NonNull List<T> findInstances(@NonNull Class<T> clazz);
 
     /**
      * A callback invoked at the entry to an instrumented method.
diff --git a/inspection/inspection/src/main/java/androidx/inspection/ArtToolingImpl.java b/inspection/inspection/src/main/java/androidx/inspection/ArtToolingImpl.java
index 04a70dd..9185d2d 100644
--- a/inspection/inspection/src/main/java/androidx/inspection/ArtToolingImpl.java
+++ b/inspection/inspection/src/main/java/androidx/inspection/ArtToolingImpl.java
@@ -18,12 +18,13 @@
 
 import android.annotation.SuppressLint;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.inspection.ArtTooling.EntryHook;
 import androidx.inspection.ArtTooling.ExitHook;
 
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -46,8 +47,7 @@
      * We don't have a way to undo bytecode manipulations, so to avoid duplicating doing the same
      * transformations multiple times, this object lives forever.
      */
-    @NonNull
-    public static ArtToolingImpl instance() {
+    public static @NonNull ArtToolingImpl instance() {
         if (sInstance == null) {
             System.loadLibrary("art_tooling");
             sInstance = new ArtToolingImpl(createNativeArtTooling());
@@ -84,8 +84,7 @@
     /**
      * Called from DefaultArtTooling
      */
-    @NonNull
-    public static <T> List<T> findInstances(@NonNull Class<T> clazz) {
+    public static <T> @NonNull List<T> findInstances(@NonNull Class<T> clazz) {
         return Arrays.asList(nativeFindInstances(instance().mNativePtr, clazz));
     }
 
@@ -150,8 +149,8 @@
     }
 
     /** Callback from native */
-    @Nullable
-    public static Object onExit(@NonNull String methodSignature, @Nullable Object returnObject) {
+    public static @Nullable Object onExit(@NonNull String methodSignature,
+            @Nullable Object returnObject) {
         return onExitInternal(methodSignature, returnObject);
     }
 
@@ -213,7 +212,7 @@
      * receive the array: ["(Lcom/example/Receiver;Ljava/lang/String;)Lcom/example/Client;", this,
      * r, message]
      */
-    public static void onEntry(@NonNull Object[] signatureThisParams) {
+    public static void onEntry(Object @NonNull [] signatureThisParams) {
         // Should always at least contain signature and "this"
         assert (signatureThisParams.length >= 2);
         String signature = (String) signatureThisParams[0];
diff --git a/inspection/inspection/src/main/java/androidx/inspection/Connection.java b/inspection/inspection/src/main/java/androidx/inspection/Connection.java
index fd0c3f2..cd309ee 100644
--- a/inspection/inspection/src/main/java/androidx/inspection/Connection.java
+++ b/inspection/inspection/src/main/java/androidx/inspection/Connection.java
@@ -16,7 +16,7 @@
 
 package androidx.inspection;
 
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
 
 /**
  * A class representing a connection between studio and inspectors.
@@ -28,6 +28,6 @@
      *
      * @param data An array of bytes. Up to inspectors to determine how to encode bytes.
      */
-    public void sendEvent(@NonNull byte[] data) {
+    public void sendEvent(byte @NonNull [] data) {
     }
 }
diff --git a/inspection/inspection/src/main/java/androidx/inspection/DefaultArtTooling.java b/inspection/inspection/src/main/java/androidx/inspection/DefaultArtTooling.java
index fcceef9..6a67de7 100644
--- a/inspection/inspection/src/main/java/androidx/inspection/DefaultArtTooling.java
+++ b/inspection/inspection/src/main/java/androidx/inspection/DefaultArtTooling.java
@@ -16,9 +16,10 @@
 
 package androidx.inspection;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 
+import org.jspecify.annotations.NonNull;
+
 import java.util.List;
 
 /**
@@ -32,9 +33,8 @@
         mInspectorId = inspectorId;
     }
 
-    @NonNull
     @Override
-    public <T> List<T> findInstances(@NonNull Class<T> clazz) {
+    public <T> @NonNull List<T> findInstances(@NonNull Class<T> clazz) {
         return ArtToolingImpl.findInstances(clazz);
     }
 
diff --git a/inspection/inspection/src/main/java/androidx/inspection/Inspector.java b/inspection/inspection/src/main/java/androidx/inspection/Inspector.java
index f512df2..fe0506d 100644
--- a/inspection/inspection/src/main/java/androidx/inspection/Inspector.java
+++ b/inspection/inspection/src/main/java/androidx/inspection/Inspector.java
@@ -18,7 +18,7 @@
 
 import android.annotation.SuppressLint;
 
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
 
 import java.util.concurrent.Executor;
 
@@ -30,8 +30,7 @@
  */
 public abstract class Inspector {
 
-    @NonNull
-    private Connection mConnection;
+    private @NonNull Connection mConnection;
 
     /**
      * @param connection a connection object that allows to send events to studio
@@ -57,13 +56,12 @@
      * @param data a raw byte array of the command sent by studio.
      * @param callback a callback to reply on the given command.
      */
-    public abstract void onReceiveCommand(@NonNull byte[] data, @NonNull CommandCallback callback);
+    public abstract void onReceiveCommand(byte @NonNull [] data, @NonNull CommandCallback callback);
 
     /**
      * Returns a connection that allows to send events to Studio.
      */
-    @NonNull
-    protected final Connection getConnection() {
+    protected final @NonNull Connection getConnection() {
         return mConnection;
     }
 
@@ -78,7 +76,7 @@
          */
         // Users don't implement this callback, but call methods on it themselves
         @SuppressLint("CallbackMethodName")
-        void reply(@NonNull byte[] response);
+        void reply(byte @NonNull [] response);
 
         /**
          * Handles a signal sent from Studio that this command should be cancelled, if possible.
diff --git a/inspection/inspection/src/main/java/androidx/inspection/InspectorEnvironment.java b/inspection/inspection/src/main/java/androidx/inspection/InspectorEnvironment.java
index c8b22eb..e70c992 100644
--- a/inspection/inspection/src/main/java/androidx/inspection/InspectorEnvironment.java
+++ b/inspection/inspection/src/main/java/androidx/inspection/InspectorEnvironment.java
@@ -16,7 +16,7 @@
 
 package androidx.inspection;
 
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
 
 /**
  * This interface provides inspector specific utilities, such as
@@ -28,14 +28,12 @@
      * Executors provided by App Inspection Platforms. Clients should use it instead of
      * creating their own.
      */
-    @NonNull
-    default InspectorExecutors executors() {
+    default @NonNull InspectorExecutors executors() {
         throw new UnsupportedOperationException();
     }
 
     /**
      * Interface that provides ART TI capabilities.
      */
-    @NonNull
-    ArtTooling artTooling();
+    @NonNull ArtTooling artTooling();
 }
diff --git a/inspection/inspection/src/main/java/androidx/inspection/InspectorExecutors.java b/inspection/inspection/src/main/java/androidx/inspection/InspectorExecutors.java
index 4bb081e..09ebfa5 100644
--- a/inspection/inspection/src/main/java/androidx/inspection/InspectorExecutors.java
+++ b/inspection/inspection/src/main/java/androidx/inspection/InspectorExecutors.java
@@ -18,7 +18,7 @@
 
 import android.os.Handler;
 
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
 
 import java.util.concurrent.Executor;
 
@@ -57,8 +57,7 @@
      * Even when a future wasn't dropped or lost, developers would still need to block one
      * of the threads.
      */
-    @NonNull
-    Handler handler();
+    @NonNull Handler handler();
 
     /**
      * Primary single threaded executor for the given inspector.
@@ -70,12 +69,10 @@
      * It is important to keep this executor responsive, so it can quickly process incoming
      * messages.
      */
-    @NonNull
-    Executor primary();
+    @NonNull Executor primary();
 
     /**
      * An executor for offloading blocking IO tasks to a shared pool of threads.
      */
-    @NonNull
-    Executor io();
+    @NonNull Executor io();
 }
diff --git a/inspection/inspection/src/main/java/androidx/inspection/InspectorFactory.java b/inspection/inspection/src/main/java/androidx/inspection/InspectorFactory.java
index 2f4c253..cbf8399 100644
--- a/inspection/inspection/src/main/java/androidx/inspection/InspectorFactory.java
+++ b/inspection/inspection/src/main/java/androidx/inspection/InspectorFactory.java
@@ -16,7 +16,7 @@
 
 package androidx.inspection;
 
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
 
 /**
  * A factory that is responsible for creation of an inspector for your library.
@@ -42,8 +42,7 @@
     /**
      * @return an id of an inspector that is served by this factory.
      */
-    @NonNull
-    public final String getInspectorId() {
+    public final @NonNull String getInspectorId() {
         return mInspectorId;
     }
 
@@ -54,7 +53,6 @@
      * @param environment an environment that provides tooling utilities.
      * @return a new instance of an inspector.
      */
-    @NonNull
-    public abstract T createInspector(@NonNull Connection connection,
+    public abstract @NonNull T createInspector(@NonNull Connection connection,
             @NonNull InspectorEnvironment environment);
 }
diff --git a/libraryversions.toml b/libraryversions.toml
index 0546928..f27ab31 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -22,8 +22,8 @@
 CAR_APP = "1.8.0-alpha01"
 COLLECTION = "1.5.0-beta02"
 COMPOSE = "1.8.0-alpha08"
-COMPOSE_MATERIAL3 = "1.4.0-alpha06"
-COMPOSE_MATERIAL3_ADAPTIVE = "1.1.0-alpha09"
+COMPOSE_MATERIAL3 = "1.4.0-alpha07"
+COMPOSE_MATERIAL3_ADAPTIVE = "1.1.0-beta01"
 COMPOSE_MATERIAL3_COMMON = "1.0.0-alpha01"
 COMPOSE_MATERIAL3_XR = "1.0.0-alpha02"
 COMPOSE_RUNTIME = "1.8.0-beta01"
@@ -64,7 +64,7 @@
 EMOJI = "1.2.0-alpha03"
 EMOJI2 = "1.5.0-rc01"
 ENTERPRISE = "1.1.0-rc01"
-EXIFINTERFACE = "1.4.0-beta01"
+EXIFINTERFACE = "1.4.0-rc01"
 FRAGMENT = "1.9.0-alpha01"
 FUTURES = "1.3.0-alpha01"
 GLANCE = "1.2.0-alpha01"
@@ -106,7 +106,7 @@
 NAVIGATION3 = "0.1.0-dev01"
 PAGING = "3.4.0-alpha01"
 PALETTE = "1.1.0-alpha01"
-PDF = "1.0.0-alpha05"
+PDF = "1.0.0-alpha06"
 PERCENTLAYOUT = "1.1.0-alpha01"
 PERFORMANCE = "1.0.0-alpha01"
 PREFERENCE = "1.3.0-alpha01"
@@ -115,8 +115,8 @@
 PRIVACYSANDBOX_ADS = "1.1.0-beta12"
 PRIVACYSANDBOX_PLUGINS = "1.0.0-alpha03"
 PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha16"
-PRIVACYSANDBOX_TOOLS = "1.0.0-alpha10"
-PRIVACYSANDBOX_UI = "1.0.0-alpha12"
+PRIVACYSANDBOX_TOOLS = "1.0.0-alpha11"
+PRIVACYSANDBOX_UI = "1.0.0-alpha13"
 PROFILEINSTALLER = "1.5.0-alpha01"
 RECOMMENDATION = "1.1.0-alpha01"
 RECYCLERVIEW = "1.5.0-alpha01"
@@ -132,7 +132,7 @@
 SECURITY_BIOMETRIC = "1.0.0-alpha01"
 SECURITY_IDENTITY_CREDENTIAL = "1.0.0-alpha04"
 SECURITY_MLS = "1.0.0-alpha01"
-SECURITY_STATE = "1.0.0-alpha04"
+SECURITY_STATE = "1.0.0-alpha05"
 SECURITY_STATE_PROVIDER = "1.0.0-alpha01"
 SHARETARGET = "1.3.0-alpha01"
 SLICE = "1.1.0-alpha03"
@@ -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-compose/api/current.txt b/lifecycle/lifecycle-viewmodel-compose/api/current.txt
index ace8748..8372225 100644
--- a/lifecycle/lifecycle-viewmodel-compose/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel-compose/api/current.txt
@@ -29,3 +29,12 @@
 
 }
 
+package androidx.lifecycle.viewmodel.compose.serialization.serializers {
+
+  public final class MutableStateSerializerKt {
+    method public static inline <reified T> kotlinx.serialization.KSerializer<androidx.compose.runtime.MutableState<T>> MutableStateSerializer();
+    method public static <T> kotlinx.serialization.KSerializer<androidx.compose.runtime.MutableState<T>> MutableStateSerializer(kotlinx.serialization.KSerializer<T> serializer);
+  }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
index ace8748..8372225 100644
--- a/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
@@ -29,3 +29,12 @@
 
 }
 
+package androidx.lifecycle.viewmodel.compose.serialization.serializers {
+
+  public final class MutableStateSerializerKt {
+    method public static inline <reified T> kotlinx.serialization.KSerializer<androidx.compose.runtime.MutableState<T>> MutableStateSerializer();
+    method public static <T> kotlinx.serialization.KSerializer<androidx.compose.runtime.MutableState<T>> MutableStateSerializer(kotlinx.serialization.KSerializer<T> serializer);
+  }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index c63ee18..6b1230c 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -30,6 +30,7 @@
     id("AndroidXPlugin")
     id("com.android.library")
     id("AndroidXComposePlugin")
+    alias(libs.plugins.kotlinSerialization)
 }
 
 androidXMultiplatform {
@@ -45,12 +46,17 @@
                 api(project(":lifecycle:lifecycle-viewmodel"))
                 api("androidx.annotation:annotation:1.8.1")
                 api("androidx.compose.runtime:runtime:1.6.0")
+                api(libs.kotlinSerializationCore)
                 implementation(libs.kotlinStdlib)
             }
         }
 
         commonTest {
-            // TODO(b/330323282): Move common dependencies here.
+            dependencies {
+                implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
+                implementation(project(":lifecycle:lifecycle-viewmodel-testing"))
+                implementation(project(":lifecycle:lifecycle-runtime-testing"))
+            }
         }
 
         androidMain {
@@ -83,9 +89,7 @@
                 // but it doesn't work in androidx.
                 // See aosp/1804059
                 implementation(project(":lifecycle:lifecycle-common-java8"))
-                implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
                 implementation(project(":activity:activity-compose"))
-                implementation(project(":lifecycle:lifecycle-runtime-testing"))
             }
         }
     }
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/serialization/serializers/MutableStateSerializerTest.android.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/serialization/serializers/MutableStateSerializerTest.android.kt
new file mode 100644
index 0000000..0cbeb73
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/serialization/serializers/MutableStateSerializerTest.android.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.lifecycle.viewmodel.compose.serialization.serializers
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.savedstate.serialization.decodeFromSavedState
+import androidx.savedstate.serialization.encodeToSavedState
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MutableStateSerializerTest {
+
+    @Test
+    fun encodeDecode_withImplicitSerializer() {
+        val state = mutableStateOf(USER_JOHN_DOE)
+        val serializer = MutableStateSerializer<User>()
+
+        val encoded = encodeToSavedState(serializer, state)
+        val decoded = decodeFromSavedState(serializer, encoded)
+
+        assertThat(state.value).isEqualTo(decoded.value)
+    }
+
+    @Test
+    fun encodeDecode_withExplicitSerializer() {
+        val state = mutableStateOf(USER_JOHN_DOE)
+        val serializer = MutableStateSerializer(USER_SERIALIZER)
+
+        val encoded = encodeToSavedState(serializer, state)
+        val decoded = decodeFromSavedState(serializer, encoded)
+
+        assertThat(state.value).isEqualTo(decoded.value)
+    }
+
+    companion object {
+        val USER_JOHN_DOE = User(name = "John", surname = "Doe")
+        @OptIn(InternalSerializationApi::class) val USER_SERIALIZER = User::class.serializer()
+    }
+
+    @Serializable data class User(val name: String = "John", val surname: String = "Doe")
+}
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/commonMain/kotlin/androidx/lifecycle/viewmodel/compose/serialization/serializers/MutableStateSerializer.kt b/lifecycle/lifecycle-viewmodel-compose/src/commonMain/kotlin/androidx/lifecycle/viewmodel/compose/serialization/serializers/MutableStateSerializer.kt
new file mode 100644
index 0000000..1bd43bd
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-compose/src/commonMain/kotlin/androidx/lifecycle/viewmodel/compose/serialization/serializers/MutableStateSerializer.kt
@@ -0,0 +1,83 @@
+/*
+ * 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:OptIn(InternalSerializationApi::class, ExperimentalTypeInference::class)
+
+package androidx.lifecycle.viewmodel.compose.serialization.serializers
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import kotlin.experimental.ExperimentalTypeInference
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.serializer
+
+/**
+ * Creates a [KSerializer] for a [MutableState] containing a [Serializable] value of type [T].
+ *
+ * This inline function infers the state type [T] automatically and retrieves the appropriate
+ * [KSerializer] for serialization and deserialization of [MutableState].
+ *
+ * @param T The type of the value stored in the [MutableState].
+ * @return A [KSerializer] for handling [MutableState] containing a [Serializable] type [T].
+ */
+@Suppress("FunctionName")
+public inline fun <reified T> MutableStateSerializer(): KSerializer<MutableState<T>> {
+    return MutableStateSerializer(serializer())
+}
+
+/**
+ * Creates a [KSerializer] for a [MutableState] containing a [Serializable] value of type [T].
+ *
+ * This function allows for explicit specification of the [KSerializer] for the state type [T]. It
+ * provides serialization and deserialization capabilities for [MutableState] objects.
+ *
+ * @param T The type of the value stored in the [MutableState].
+ * @param serializer The [KSerializer] for the [Serializable] type [T].
+ * @return A [KSerializer] for handling [MutableState] containing a [Serializable] type [T].
+ */
+@Suppress("FunctionName")
+public fun <T> MutableStateSerializer(serializer: KSerializer<T>): KSerializer<MutableState<T>> {
+    return MutableStateSerializerImpl<T>(serializer)
+}
+
+/**
+ * Internal implementation of [KSerializer] for [MutableState].
+ *
+ * This private class wraps a [KSerializer] for the inner value type [T], enabling serialization and
+ * deserialization of [MutableState] instances. The inner value serialization is delegated to the
+ * provided [valueSerializer].
+ *
+ * @param T The type of the value stored in the [MutableState].
+ * @property valueSerializer The [KSerializer] used to serialize and deserialize the inner value.
+ */
+private class MutableStateSerializerImpl<T>(
+    private val valueSerializer: KSerializer<T>,
+) : KSerializer<MutableState<T>> {
+
+    override val descriptor: SerialDescriptor = valueSerializer.descriptor
+
+    override fun serialize(encoder: Encoder, value: MutableState<T>) {
+        valueSerializer.serialize(encoder, value.value)
+    }
+
+    override fun deserialize(decoder: Decoder): MutableState<T> {
+        return mutableStateOf(valueSerializer.deserialize(decoder))
+    }
+}
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/ViewModelStoreNavLocalProviderTest.kt
similarity index 70%
rename from lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
rename to lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavLocalProviderTest.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/ViewModelStoreNavLocalProviderTest.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/ViewModelStoreNavLocalProvider.android.kt
similarity index 75%
rename from lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
rename to lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavLocalProvider.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/ViewModelStoreNavLocalProvider.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/mediarouter/mediarouter/api/current.txt b/mediarouter/mediarouter/api/current.txt
index 8b9f3d9..644df6d 100644
--- a/mediarouter/mediarouter/api/current.txt
+++ b/mediarouter/mediarouter/api/current.txt
@@ -406,7 +406,7 @@
     field public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1; // 0x1
     field public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 4; // 0x4
     field public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 2; // 0x2
-    field public static final int REASON_DISCONNECTED = 1; // 0x1
+    field public static final int REASON_DISCONNECT_CALLED = 1; // 0x1
     field public static final int REASON_FAILED_TO_CREATE_DYNAMIC_GROUP_ROUTE_CONTROLLER = 6; // 0x6
     field public static final int REASON_REJECTED_FOR_SELECTED_ROUTE = 4; // 0x4
     field public static final int REASON_ROUTE_CONNECTION_TIMEOUT = 7; // 0x7
diff --git a/mediarouter/mediarouter/api/restricted_current.txt b/mediarouter/mediarouter/api/restricted_current.txt
index 8b9f3d9..644df6d 100644
--- a/mediarouter/mediarouter/api/restricted_current.txt
+++ b/mediarouter/mediarouter/api/restricted_current.txt
@@ -406,7 +406,7 @@
     field public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1; // 0x1
     field public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 4; // 0x4
     field public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 2; // 0x2
-    field public static final int REASON_DISCONNECTED = 1; // 0x1
+    field public static final int REASON_DISCONNECT_CALLED = 1; // 0x1
     field public static final int REASON_FAILED_TO_CREATE_DYNAMIC_GROUP_ROUTE_CONTROLLER = 6; // 0x6
     field public static final int REASON_REJECTED_FOR_SELECTED_ROUTE = 4; // 0x4
     field public static final int REASON_ROUTE_CONNECTION_TIMEOUT = 7; // 0x7
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
index 5722db5..141a97b 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
@@ -196,7 +196,7 @@
         assertNotNull(mRequestedRoute);
         assertEquals(ROUTE_ID_2, mRequestedRoute.getDescriptorId());
         assertEquals(RouteConnectionState.STATE_DISCONNECTED, mRouteConnectionState);
-        assertEquals(MediaRouter.REASON_DISCONNECTED, mRouteDisconnectedReason);
+        assertEquals(MediaRouter.REASON_DISCONNECT_CALLED, mRouteDisconnectedReason);
     }
 
     @Test()
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index c7ea94e..e2e0825 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -1686,7 +1686,7 @@
         /* package */ void disconnect() {
             mController.onUnselect(UNSELECT_REASON_DISCONNECTED);
             mController.onRelease();
-            notifyRouteDisconnected(MediaRouter.REASON_DISCONNECTED);
+            notifyRouteDisconnected(MediaRouter.REASON_DISCONNECT_CALLED);
         }
 
         @Override
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index af4bd6d..0b481f2 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -130,7 +130,7 @@
     public static final int UNSELECT_REASON_ROUTE_CHANGED = 3;
 
     @IntDef({
-        REASON_DISCONNECTED,
+        REASON_DISCONNECT_CALLED,
         REASON_ROUTE_NOT_AVAILABLE,
         REASON_ROUTE_NOT_ENABLED,
         REASON_REJECTED_FOR_SELECTED_ROUTE,
@@ -146,7 +146,7 @@
      *
      * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
-    public static final int REASON_DISCONNECTED = 1;
+    public static final int REASON_DISCONNECT_CALLED = 1;
 
     /**
      * The route connection has failed because the requested route is no longer available.
@@ -165,6 +165,9 @@
     /**
      * The route connection has failed because the requested route is a selected route.
      *
+     * <p>If a route is already selected, then calling {@link RouteInfo#connect()} on the selected
+     * route will be rejected and do nothing.
+     *
      * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
     public static final int REASON_REJECTED_FOR_SELECTED_ROUTE = 4;
@@ -1615,15 +1618,19 @@
         /**
          * Returns true if this route is currently selected.
          *
+         * <p>Only one representative route can return true. For instance:
+         *
+         * <ul>
+         *   <li>If this route is a selected (non-group) route, it returns true.
+         *   <li>If this route is a selected group route, it returns true.
+         *   <li>If this route is a selected member route of a group, it returns false.
+         * </ul>
+         *
          * <p>Must be called on the main thread.
          *
          * @return True if this route is currently selected.
          * @see MediaRouter#getSelectedRoute
          */
-        // Note: Only one representative route can return true. For instance:
-        //   - If this route is a selected (non-group) route, it returns true.
-        //   - If this route is a selected group route, it returns true.
-        //   - If this route is a selected member route of a group, it returns false.
         @MainThread
         public boolean isSelected() {
             checkCallingThread();
@@ -2062,7 +2069,10 @@
         /**
          * Connects this route without selecting it.
          *
-         * <p>If the route is already selected, connecting this route will do nothing.
+         * <p>Apps can select a route by calling {@link #select()}. If apps want to keep the
+         * selected route unchanged and connect to additional routes, then they can use this method
+         * to connect additional routes. If the route is already selected, connecting this route
+         * will do nothing.
          *
          * <p>Must be called on the main thread.
          */
@@ -2849,7 +2859,8 @@
          * routes while connecting to other routes.
          *
          * <p>The connected route could be different from the route requested by {@link
-         * RouteInfo#connect()}.
+         * RouteInfo#connect()}. This can happen when the {@link MediaTransferReceiver media
+         * transfer feature} is enabled.
          *
          * @param router the media router reporting the event.
          * @param connectedRoute the route that has been connected.
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
index 0a5ea75..f34aa4c 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
@@ -687,8 +687,9 @@
             // animating. In these cases the currentEntry will be null, and in those cases,
             // AnimatedContent will just skip attempting to transition the old entry.
             // See https://issuetracker.google.com/238686802
+            val isPredictiveBackCancelAnimation = transitionState.currentState == backStackEntry
             val currentEntry =
-                if (inPredictiveBack) {
+                if (inPredictiveBack || isPredictiveBackCancelAnimation) {
                     // We have to do this because the previous entry does not show up in
                     // visibleEntries
                     // even if we prepare it above as part of onBackStackChangeStarted
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
index 4fb24ac..6838cad 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
@@ -21,7 +21,6 @@
 import android.content.ContextWrapper
 import android.content.Intent
 import android.net.Uri
-import android.os.Bundle
 import android.util.AttributeSet
 import android.util.Log
 import androidx.annotation.CallSuper
@@ -29,6 +28,8 @@
 import androidx.core.app.ActivityCompat
 import androidx.core.app.ActivityOptionsCompat
 import androidx.core.content.res.use
+import androidx.savedstate.SavedState
+import androidx.savedstate.read
 import java.util.regex.Pattern
 
 /** ActivityNavigator implements cross-activity navigation. */
@@ -75,7 +76,7 @@
     @Suppress("DEPRECATION")
     override fun navigate(
         destination: Destination,
-        args: Bundle?,
+        args: SavedState?,
         navOptions: NavOptions?,
         navigatorExtras: Navigator.Extras?
     ): NavDestination? {
@@ -92,17 +93,19 @@
                 val fillInPattern = Pattern.compile("\\{(.+?)\\}")
                 val matcher = fillInPattern.matcher(dataPattern)
                 while (matcher.find()) {
-                    val argName = matcher.group(1)
-                    require(args.containsKey(argName)) {
-                        "Could not find $argName in $args to fill data pattern $dataPattern"
+                    args.read {
+                        val argName = matcher.group(1)!!
+                        require(contains(argName)) {
+                            "Could not find $argName in $args to fill data pattern $dataPattern"
+                        }
+                        matcher.appendReplacement(data, "")
+                        // Serialize with NavType if present, otherwise fallback to toString()
+                        val navType = destination.arguments[argName]?.type
+                        val value =
+                            navType?.serializeAsValue(navType[args, argName])
+                                ?: Uri.encode(args.get(argName).toString())
+                        data.append(value)
                     }
-                    matcher.appendReplacement(data, "")
-                    // Serialize with NavType if present, otherwise fallback to toString()
-                    val navType = destination.arguments[argName!!]?.type
-                    val value =
-                        navType?.serializeAsValue(navType[args, argName])
-                            ?: Uri.encode(args[argName].toString())
-                    data.append(value)
                 }
                 matcher.appendTail(data)
                 intent.data = Uri.parse(data.toString())
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavBackStackEntryState.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavBackStackEntryState.kt
index 2fec191..be20880 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavBackStackEntryState.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavBackStackEntryState.kt
@@ -17,23 +17,23 @@
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.os.Bundle
 import android.os.Parcel
 import android.os.Parcelable
 import androidx.lifecycle.Lifecycle
+import androidx.savedstate.SavedState
 
 @SuppressLint("BanParcelableUsage")
 internal class NavBackStackEntryState : Parcelable {
     val id: String
     val destinationId: Int
-    val args: Bundle?
-    val savedState: Bundle
+    val args: SavedState?
+    val savedState: SavedState
 
     constructor(entry: NavBackStackEntry) {
         id = entry.id
         destinationId = entry.destination.id
         args = entry.arguments
-        savedState = Bundle()
+        savedState = SavedState()
         entry.saveState(savedState)
     }
 
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index d592f33..4eed05f 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -21,7 +21,6 @@
 import android.content.ContextWrapper
 import android.content.Intent
 import android.net.Uri
-import android.os.Bundle
 import android.os.Parcelable
 import android.util.Log
 import androidx.activity.OnBackPressedCallback
@@ -33,7 +32,6 @@
 import androidx.annotation.RestrictTo
 import androidx.core.app.TaskStackBuilder
 import androidx.core.net.toUri
-import androidx.core.os.bundleOf
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleObserver
@@ -46,6 +44,10 @@
 import androidx.navigation.NavGraph.Companion.findStartDestination
 import androidx.navigation.serialization.generateHashCode
 import androidx.navigation.serialization.generateRouteWithArgs
+import androidx.savedstate.SavedState
+import androidx.savedstate.read
+import androidx.savedstate.savedState
+import androidx.savedstate.write
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.atomic.AtomicInteger
 import kotlin.collections.removeFirst as removeFirstKt
@@ -109,7 +111,7 @@
             setGraph(graph, null)
         }
 
-    private var navigatorStateToRestore: Bundle? = null
+    private var navigatorStateToRestore: SavedState? = null
     private var backStackToRestore: Array<Parcelable>? = null
     private var deepLinkHandled = false
 
@@ -239,7 +241,7 @@
         public fun onDestinationChanged(
             controller: NavController,
             destination: NavDestination,
-            arguments: Bundle?
+            arguments: SavedState?
         )
     }
 
@@ -333,7 +335,7 @@
             super.push(backStackEntry)
         }
 
-        override fun createBackStackEntry(destination: NavDestination, arguments: Bundle?) =
+        override fun createBackStackEntry(destination: NavDestination, arguments: SavedState?) =
             NavBackStackEntry.create(context, destination, arguments, hostLifecycleState, viewModel)
 
         override fun pop(popUpTo: NavBackStackEntry, saveState: Boolean) {
@@ -1005,7 +1007,7 @@
         val extras = intent.extras
 
         val deepLinkIds = extras!!.getIntArray(KEY_DEEP_LINK_IDS)!!.toMutableList()
-        val deepLinkArgs = extras.getParcelableArrayList<Bundle>(KEY_DEEP_LINK_ARGS)
+        val deepLinkArgs = extras.getParcelableArrayList<SavedState>(KEY_DEEP_LINK_ARGS)
 
         // Remove the leaf destination to pop up to one level above it
         var leafDestinationId = deepLinkIds.removeLastKt()
@@ -1031,8 +1033,10 @@
         val navDeepLinkBuilder = createDeepLink()
 
         // Attach the original global arguments, and also the original calling Intent.
-        val arguments = bundleOf(KEY_DEEP_LINK_INTENT to intent)
-        extras.getBundle(KEY_DEEP_LINK_EXTRAS)?.let { arguments.putAll(it) }
+        val arguments = savedState {
+            putParcelable(KEY_DEEP_LINK_INTENT, intent)
+            extras.getBundle(KEY_DEEP_LINK_EXTRAS)?.let { putAll(it) }
+        }
         navDeepLinkBuilder.setArguments(arguments)
 
         deepLinkIds.forEachIndexed { index, deepLinkId ->
@@ -1054,29 +1058,30 @@
         var parent = currentDestination.parent
         while (parent != null) {
             if (parent.startDestinationId != destId) {
-                val args = Bundle()
-                if (activity != null && activity!!.intent != null) {
-                    val data = activity!!.intent.data
+                val args = savedState {
+                    if (activity != null && activity!!.intent != null) {
+                        val data = activity!!.intent.data
 
-                    // We were started via a URI intent.
-                    if (data != null) {
-                        // Include the original deep link Intent so the Destinations can
-                        // synthetically generate additional arguments as necessary.
-                        args.putParcelable(KEY_DEEP_LINK_INTENT, activity!!.intent)
-                        val currGraph = backQueue.getTopGraph()
-                        val matchingDeepLink =
-                            currGraph.matchDeepLinkComprehensive(
-                                navDeepLinkRequest = NavDeepLinkRequest(activity!!.intent),
-                                searchChildren = true,
-                                searchParent = true,
-                                lastVisited = currGraph
-                            )
-                        if (matchingDeepLink?.matchingArgs != null) {
-                            val destinationArgs =
-                                matchingDeepLink.destination.addInDefaultArgs(
-                                    matchingDeepLink.matchingArgs
+                        // We were started via a URI intent.
+                        if (data != null) {
+                            // Include the original deep link Intent so the Destinations can
+                            // synthetically generate additional arguments as necessary.
+                            putParcelable(KEY_DEEP_LINK_INTENT, activity!!.intent)
+                            val currGraph = backQueue.getTopGraph()
+                            val matchingDeepLink =
+                                currGraph.matchDeepLinkComprehensive(
+                                    navDeepLinkRequest = NavDeepLinkRequest(activity!!.intent),
+                                    searchChildren = true,
+                                    searchParent = true,
+                                    lastVisited = currGraph
                                 )
-                            args.putAll(destinationArgs)
+                            if (matchingDeepLink?.matchingArgs != null) {
+                                val destinationArgs =
+                                    matchingDeepLink.destination.addInDefaultArgs(
+                                        matchingDeepLink.matchingArgs
+                                    )
+                                destinationArgs?.let { putAll(it) }
+                            }
                         }
                     }
                 }
@@ -1349,7 +1354,7 @@
      */
     @MainThread
     @CallSuper
-    public open fun setGraph(@NavigationRes graphResId: Int, startDestinationArgs: Bundle?) {
+    public open fun setGraph(@NavigationRes graphResId: Int, startDestinationArgs: SavedState?) {
         setGraph(navInflater.inflate(graphResId), startDestinationArgs)
     }
 
@@ -1366,7 +1371,7 @@
      */
     @MainThread
     @CallSuper
-    public open fun setGraph(graph: NavGraph, startDestinationArgs: Bundle?) {
+    public open fun setGraph(graph: NavGraph, startDestinationArgs: SavedState?) {
         check(backQueue.isEmpty() || hostLifecycleState != Lifecycle.State.DESTROYED) {
             "You cannot set a new graph on a NavController with entries on the back stack " +
                 "after the NavController has been destroyed. Please ensure that your NavHost " +
@@ -1413,16 +1418,15 @@
     }
 
     @MainThread
-    private fun onGraphCreated(startDestinationArgs: Bundle?) {
-        navigatorStateToRestore?.let { navigatorStateToRestore ->
-            val navigatorNames =
-                navigatorStateToRestore.getStringArrayList(KEY_NAVIGATOR_STATE_NAMES)
-            if (navigatorNames != null) {
+    private fun onGraphCreated(startDestinationArgs: SavedState?) {
+        navigatorStateToRestore?.read {
+            if (contains(KEY_NAVIGATOR_STATE_NAMES)) {
+                val navigatorNames = getStringList(KEY_NAVIGATOR_STATE_NAMES)
                 for (name in navigatorNames) {
                     val navigator = _navigatorProvider.getNavigator<Navigator<*>>(name)
-                    val bundle = navigatorStateToRestore.getBundle(name)
-                    if (bundle != null) {
-                        navigator.onRestoreState(bundle)
+                    if (contains(name)) {
+                        val savedState = getSavedState(name)
+                        navigator.onRestoreState(savedState)
                     }
                 }
             }
@@ -1508,11 +1512,11 @@
                 Log.e(TAG, "handleDeepLink() could not extract deepLink from $intent", e)
                 null
             }
-        var deepLinkArgs = extras?.getParcelableArrayList<Bundle>(KEY_DEEP_LINK_ARGS)
-        val globalArgs = Bundle()
+        var deepLinkArgs = extras?.getParcelableArrayList<SavedState>(KEY_DEEP_LINK_ARGS)
+        val globalArgs = savedState()
         val deepLinkExtras = extras?.getBundle(KEY_DEEP_LINK_EXTRAS)
         if (deepLinkExtras != null) {
-            globalArgs.putAll(deepLinkExtras)
+            globalArgs.write { putAll(deepLinkExtras) }
         }
         if (deepLink == null || deepLink.isEmpty()) {
             val currGraph = backQueue.getTopGraph()
@@ -1529,7 +1533,7 @@
                 deepLinkArgs = null
                 val destinationArgs = destination.addInDefaultArgs(matchingDeepLink.matchingArgs)
                 if (destinationArgs != null) {
-                    globalArgs.putAll(destinationArgs)
+                    globalArgs.write { putAll(destinationArgs) }
                 }
             }
         }
@@ -1545,15 +1549,16 @@
             )
             return false
         }
-        globalArgs.putParcelable(KEY_DEEP_LINK_INTENT, intent)
-        val args = arrayOfNulls<Bundle>(deepLink.size)
+        globalArgs.write { putParcelable(KEY_DEEP_LINK_INTENT, intent) }
+        val args = arrayOfNulls<SavedState>(deepLink.size)
         for (index in args.indices) {
-            val arguments = Bundle()
-            arguments.putAll(globalArgs)
-            if (deepLinkArgs != null) {
-                val deepLinkArguments = deepLinkArgs[index]
-                if (deepLinkArguments != null) {
-                    arguments.putAll(deepLinkArguments)
+            val arguments = savedState {
+                putAll(globalArgs)
+                if (deepLinkArgs != null) {
+                    val deepLinkArguments = deepLinkArgs[index]
+                    if (deepLinkArguments != null) {
+                        putAll(deepLinkArguments)
+                    }
                 }
             }
             args[index] = arguments
@@ -1604,15 +1609,15 @@
         if (matchingDeepLink != null) {
             val destination = matchingDeepLink.destination
             val deepLink = destination.buildDeepLinkIds()
-            val globalArgs = Bundle()
-            val destinationArgs = destination.addInDefaultArgs(matchingDeepLink.matchingArgs)
-            if (destinationArgs != null) {
-                globalArgs.putAll(destinationArgs)
+            val globalArgs = savedState {
+                val destinationArgs = destination.addInDefaultArgs(matchingDeepLink.matchingArgs)
+                if (destinationArgs != null) {
+                    putAll(destinationArgs)
+                }
             }
-            val args = arrayOfNulls<Bundle>(deepLink.size)
+            val args = arrayOfNulls<SavedState>(deepLink.size)
             for (index in args.indices) {
-                val arguments = Bundle()
-                arguments.putAll(globalArgs)
+                val arguments = savedState { putAll(globalArgs) }
                 args[index] = arguments
             }
             return handleDeepLink(deepLink, args, true)
@@ -1622,7 +1627,7 @@
 
     private fun handleDeepLink(
         deepLink: IntArray,
-        args: Array<Bundle?>,
+        args: Array<SavedState?>,
         newTask: Boolean
     ): Boolean {
         if (newTask) {
@@ -1883,7 +1888,7 @@
      *   destination
      */
     @MainThread
-    public open fun navigate(@IdRes resId: Int, args: Bundle?) {
+    public open fun navigate(@IdRes resId: Int, args: SavedState?) {
         navigate(resId, args, null)
     }
 
@@ -1902,7 +1907,7 @@
      *   destination
      */
     @MainThread
-    public open fun navigate(@IdRes resId: Int, args: Bundle?, navOptions: NavOptions?) {
+    public open fun navigate(@IdRes resId: Int, args: SavedState?, navOptions: NavOptions?) {
         navigate(resId, args, navOptions, null)
     }
 
@@ -1925,7 +1930,7 @@
     @MainThread
     public open fun navigate(
         @IdRes resId: Int,
-        args: Bundle?,
+        args: SavedState?,
         navOptions: NavOptions?,
         navigatorExtras: Navigator.Extras?
     ) {
@@ -1939,7 +1944,7 @@
 
         @IdRes var destId = resId
         val navAction = currentNode.getAction(resId)
-        var combinedArgs: Bundle? = null
+        var combinedArgs: SavedState? = null
         if (navAction != null) {
             if (finalNavOptions == null) {
                 finalNavOptions = navAction.navOptions
@@ -1947,15 +1952,14 @@
             destId = navAction.destinationId
             val navActionArgs = navAction.defaultArguments
             if (navActionArgs != null) {
-                combinedArgs = Bundle()
-                combinedArgs.putAll(navActionArgs)
+                combinedArgs = savedState { putAll(navActionArgs) }
             }
         }
         if (args != null) {
             if (combinedArgs == null) {
-                combinedArgs = Bundle()
+                combinedArgs = savedState()
             }
-            combinedArgs.putAll(args)
+            combinedArgs.write { putAll(args) }
         }
         // just pop and return if destId is invalid
         if (
@@ -2109,14 +2113,14 @@
             )
         if (deepLinkMatch != null) {
             val destination = deepLinkMatch.destination
-            val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
+            val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: savedState()
             val node = deepLinkMatch.destination
             val intent =
                 Intent().apply {
                     setDataAndType(request.uri, request.mimeType)
                     action = request.action
                 }
-            args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
+            args.write { putParcelable(KEY_DEEP_LINK_INTENT, intent) }
             navigate(node, args, navOptions, navigatorExtras)
         } else {
             throw IllegalArgumentException(
@@ -2130,7 +2134,7 @@
     @MainThread
     private fun navigate(
         node: NavDestination,
-        args: Bundle?,
+        args: SavedState?,
         navOptions: NavOptions?,
         navigatorExtras: Navigator.Extras?
     ) {
@@ -2205,7 +2209,7 @@
         }
     }
 
-    private fun launchSingleTopInternal(node: NavDestination, args: Bundle?): Boolean {
+    private fun launchSingleTopInternal(node: NavDestination, args: SavedState?): Boolean {
         val currentBackStackEntry = currentBackStackEntry
         val nodeIndex = backQueue.indexOfLast { it.destination === node }
         // early return when node isn't even in backQueue
@@ -2254,7 +2258,7 @@
 
     private fun restoreStateInternal(
         id: Int,
-        args: Bundle?,
+        args: SavedState?,
         navOptions: NavOptions?,
         navigatorExtras: Navigator.Extras?
     ): Boolean {
@@ -2303,7 +2307,7 @@
 
     private fun executeRestoreState(
         entries: List<NavBackStackEntry>,
-        args: Bundle?,
+        args: SavedState?,
         navOptions: NavOptions?,
         navigatorExtras: Navigator.Extras?
     ): Boolean {
@@ -2373,7 +2377,7 @@
 
     private fun addEntryToBackStack(
         node: NavDestination,
-        finalArgs: Bundle?,
+        finalArgs: SavedState?,
         backStackEntry: NavBackStackEntry,
         restoredEntries: List<NavBackStackEntry> = emptyList()
     ) {
@@ -2430,7 +2434,7 @@
         ) {
             val parent = destination.parent
             if (parent != null) {
-                val args = if (finalArgs?.isEmpty == true) null else finalArgs
+                val args = if (finalArgs?.read { isEmpty() } == true) null else finalArgs
                 val entry =
                     restoredEntries.lastOrNull { restoredEntry ->
                         restoredEntry.destination == parent
@@ -2580,14 +2584,14 @@
             )
         if (deepLinkMatch != null) {
             val destination = deepLinkMatch.destination
-            val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
+            val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: savedState()
             val node = deepLinkMatch.destination
             val intent =
                 Intent().apply {
                     setDataAndType(createRoute(destination.route).toUri(), null)
                     action = null
                 }
-            args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
+            args.write { putParcelable(KEY_DEEP_LINK_INTENT, intent) }
             navigate(node, args, navOptions, navigatorExtras)
         } else {
             throw IllegalArgumentException(
@@ -2649,44 +2653,45 @@
     }
 
     /**
-     * Saves all navigation controller state to a Bundle.
+     * Saves all navigation controller state to a SavedState.
      *
-     * State may be restored from a bundle returned from this method by calling [restoreState].
+     * State may be restored from a SavedState returned from this method by calling [restoreState].
      * Saving controller state is the responsibility of a [NavHost].
      *
      * @return saved state for this controller
      */
     @CallSuper
-    public open fun saveState(): Bundle? {
-        var b: Bundle? = null
+    public open fun saveState(): SavedState? {
+        var b: SavedState? = null
         val navigatorNames = ArrayList<String>()
-        val navigatorState = Bundle()
+        val navigatorState = savedState()
         for ((name, value) in _navigatorProvider.navigators) {
             val savedState = value.onSaveState()
             if (savedState != null) {
                 navigatorNames.add(name)
-                navigatorState.putBundle(name, savedState)
+                navigatorState.write { putSavedState(name, savedState) }
             }
         }
         if (navigatorNames.isNotEmpty()) {
-            b = Bundle()
-            navigatorState.putStringArrayList(KEY_NAVIGATOR_STATE_NAMES, navigatorNames)
-            b.putBundle(KEY_NAVIGATOR_STATE, navigatorState)
+            b = savedState {
+                navigatorState.write { putStringList(KEY_NAVIGATOR_STATE_NAMES, navigatorNames) }
+                putSavedState(KEY_NAVIGATOR_STATE, navigatorState)
+            }
         }
         if (backQueue.isNotEmpty()) {
             if (b == null) {
-                b = Bundle()
+                b = savedState()
             }
             val backStack = arrayOfNulls<Parcelable>(backQueue.size)
             var index = 0
             for (backStackEntry in this.backQueue) {
                 backStack[index++] = NavBackStackEntryState(backStackEntry)
             }
-            b.putParcelableArray(KEY_BACK_STACK, backStack)
+            b.write { putParcelableList(KEY_BACK_STACK, backStack.toList().filterNotNull()) }
         }
         if (backStackMap.isNotEmpty()) {
             if (b == null) {
-                b = Bundle()
+                b = savedState()
             }
             val backStackDestIds = IntArray(backStackMap.size)
             val backStackIds = ArrayList<String?>()
@@ -2695,12 +2700,14 @@
                 backStackDestIds[index++] = destId
                 backStackIds += id
             }
-            b.putIntArray(KEY_BACK_STACK_DEST_IDS, backStackDestIds)
-            b.putStringArrayList(KEY_BACK_STACK_IDS, backStackIds)
+            b.write {
+                putIntArray(KEY_BACK_STACK_DEST_IDS, backStackDestIds)
+                putStringList(KEY_BACK_STACK_IDS, backStackIds.toList().filterNotNull())
+            }
         }
         if (backStackStates.isNotEmpty()) {
             if (b == null) {
-                b = Bundle()
+                b = savedState()
             }
             val backStackStateIds = ArrayList<String>()
             for ((id, backStackStates) in backStackStates) {
@@ -2709,56 +2716,73 @@
                 backStackStates.forEachIndexed { stateIndex, backStackState ->
                     states[stateIndex] = backStackState
                 }
-                b.putParcelableArray(KEY_BACK_STACK_STATES_PREFIX + id, states)
+                b.write {
+                    putParcelableList(
+                        KEY_BACK_STACK_STATES_PREFIX + id,
+                        states.toList().filterNotNull()
+                    )
+                }
             }
-            b.putStringArrayList(KEY_BACK_STACK_STATES_IDS, backStackStateIds)
+            b.write { putStringList(KEY_BACK_STACK_STATES_IDS, backStackStateIds) }
         }
         if (deepLinkHandled) {
             if (b == null) {
-                b = Bundle()
+                b = savedState()
             }
-            b.putBoolean(KEY_DEEP_LINK_HANDLED, deepLinkHandled)
+            b.write { putBoolean(KEY_DEEP_LINK_HANDLED, deepLinkHandled) }
         }
         return b
     }
 
     /**
-     * Restores all navigation controller state from a bundle. This should be called before any call
-     * to [setGraph].
+     * Restores all navigation controller state from a SavedState. This should be called before any
+     * call to [setGraph].
      *
-     * State may be saved to a bundle by calling [saveState]. Restoring controller state is the
+     * State may be saved to a SavedState by calling [saveState]. Restoring controller state is the
      * responsibility of a [NavHost].
      *
-     * @param navState state bundle to restore
+     * @param navState SavedState to restore
      */
     @CallSuper
-    @Suppress("DEPRECATION")
-    public open fun restoreState(navState: Bundle?) {
+    public open fun restoreState(navState: SavedState?) {
         if (navState == null) {
             return
         }
         navState.classLoader = context.classLoader
-        navigatorStateToRestore = navState.getBundle(KEY_NAVIGATOR_STATE)
-        backStackToRestore = navState.getParcelableArray(KEY_BACK_STACK)
-        backStackStates.clear()
-        val backStackDestIds = navState.getIntArray(KEY_BACK_STACK_DEST_IDS)
-        val backStackIds = navState.getStringArrayList(KEY_BACK_STACK_IDS)
-        if (backStackDestIds != null && backStackIds != null) {
-            backStackDestIds.forEachIndexed { index, id -> backStackMap[id] = backStackIds[index] }
-        }
-        val backStackStateIds = navState.getStringArrayList(KEY_BACK_STACK_STATES_IDS)
-        backStackStateIds?.forEach { id ->
-            val backStackState = navState.getParcelableArray(KEY_BACK_STACK_STATES_PREFIX + id)
-            if (backStackState != null) {
-                backStackStates[id] =
-                    ArrayDeque<NavBackStackEntryState>(backStackState.size).apply {
-                        for (parcelable in backStackState) {
-                            add(parcelable as NavBackStackEntryState)
-                        }
-                    }
+        navState.read {
+            navigatorStateToRestore =
+                if (contains(KEY_NAVIGATOR_STATE)) {
+                    getSavedState(KEY_NAVIGATOR_STATE)
+                } else null
+            backStackToRestore =
+                if (contains(KEY_BACK_STACK)) {
+                    getParcelableList<Parcelable>(KEY_BACK_STACK).toTypedArray()
+                } else null
+            backStackStates.clear()
+            if (contains(KEY_BACK_STACK_DEST_IDS) && contains(KEY_BACK_STACK_IDS)) {
+                val backStackDestIds = getIntArray(KEY_BACK_STACK_DEST_IDS)
+                val backStackIds = getStringArray(KEY_BACK_STACK_IDS)
+                backStackDestIds.forEachIndexed { index, id ->
+                    backStackMap[id] = backStackIds[index]
+                }
             }
+            if (contains(KEY_BACK_STACK_STATES_IDS)) {
+                val backStackStateIds = getStringArray(KEY_BACK_STACK_STATES_IDS)
+                backStackStateIds.forEach { id ->
+                    if (contains(KEY_BACK_STACK_STATES_PREFIX + id)) {
+                        val backStackState =
+                            getParcelableList<Parcelable>(KEY_BACK_STACK_STATES_PREFIX + id)
+                        backStackStates[id] =
+                            ArrayDeque<NavBackStackEntryState>(backStackState.size).apply {
+                                for (parcelable in backStackState) {
+                                    add(parcelable as NavBackStackEntryState)
+                                }
+                            }
+                    }
+                }
+            }
+            deepLinkHandled = getBooleanOrElse(KEY_DEEP_LINK_HANDLED) { false }
         }
-        deepLinkHandled = navState.getBoolean(KEY_DEEP_LINK_HANDLED)
     }
 
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
index 3e65714..649ba64 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
@@ -21,11 +21,12 @@
 import android.content.Context
 import android.content.ContextWrapper
 import android.content.Intent
-import android.os.Bundle
 import androidx.annotation.IdRes
 import androidx.annotation.NavigationRes
 import androidx.core.app.TaskStackBuilder
 import androidx.navigation.NavDestination.Companion.createRoute
+import androidx.savedstate.SavedState
+import androidx.savedstate.read
 
 /**
  * Class used to construct deep links to a particular destination in a [NavGraph].
@@ -50,7 +51,8 @@
  * @see NavDeepLinkBuilder.setComponentName
  */
 public class NavDeepLinkBuilder(private val context: Context) {
-    private class DeepLinkDestination constructor(val destinationId: Int, val arguments: Bundle?)
+    private class DeepLinkDestination
+    constructor(val destinationId: Int, val arguments: SavedState?)
 
     private val activity: Activity? =
         generateSequence(context) { (it as? ContextWrapper)?.baseContext }
@@ -67,7 +69,7 @@
             .also { it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) }
     private var graph: NavGraph? = null
     private val destinations = mutableListOf<DeepLinkDestination>()
-    private var globalArgs: Bundle? = null
+    private var globalArgs: SavedState? = null
 
     /** @see NavController.createDeepLink */
     internal constructor(navController: NavController) : this(navController.context) {
@@ -132,7 +134,7 @@
      * @return this object for chaining
      */
     @JvmOverloads
-    public fun setDestination(@IdRes destId: Int, args: Bundle? = null): NavDeepLinkBuilder {
+    public fun setDestination(@IdRes destId: Int, args: SavedState? = null): NavDeepLinkBuilder {
         destinations.clear()
         destinations.add(DeepLinkDestination(destId, args))
         if (graph != null) {
@@ -152,7 +154,7 @@
      * @return this object for chaining
      */
     @JvmOverloads
-    public fun setDestination(destRoute: String, args: Bundle? = null): NavDeepLinkBuilder {
+    public fun setDestination(destRoute: String, args: SavedState? = null): NavDeepLinkBuilder {
         destinations.clear()
         destinations.add(DeepLinkDestination(createRoute(destRoute).hashCode(), args))
         if (graph != null) {
@@ -190,7 +192,7 @@
      * @return this object for chaining
      */
     @JvmOverloads
-    public fun addDestination(@IdRes destId: Int, args: Bundle? = null): NavDeepLinkBuilder {
+    public fun addDestination(@IdRes destId: Int, args: SavedState? = null): NavDeepLinkBuilder {
         destinations.add(DeepLinkDestination(destId, args))
         if (graph != null) {
             verifyAllDestinations()
@@ -210,7 +212,7 @@
      * @return this object for chaining
      */
     @JvmOverloads
-    public fun addDestination(route: String, args: Bundle? = null): NavDeepLinkBuilder {
+    public fun addDestination(route: String, args: SavedState? = null): NavDeepLinkBuilder {
         destinations.add(DeepLinkDestination(createRoute(route).hashCode(), args))
         if (graph != null) {
             verifyAllDestinations()
@@ -249,7 +251,7 @@
 
     private fun fillInIntent() {
         val deepLinkIds = mutableListOf<Int>()
-        val deepLinkArgs = ArrayList<Bundle?>()
+        val deepLinkArgs = ArrayList<SavedState?>()
         var previousDestination: NavDestination? = null
         for (destination in destinations) {
             val destId = destination.destinationId
@@ -278,7 +280,7 @@
      * @param args arguments to pass to each destination
      * @return this object for chaining
      */
-    public fun setArguments(args: Bundle?): NavDeepLinkBuilder {
+    public fun setArguments(args: SavedState?): NavDeepLinkBuilder {
         globalArgs = args
         intent.putExtra(NavController.KEY_DEEP_LINK_EXTRAS, args)
         return this
@@ -330,22 +332,13 @@
      */
     @Suppress("DEPRECATION")
     public fun createPendingIntent(): PendingIntent {
-        var requestCode = 0
-        globalArgs?.let { globalArgs ->
-            for (key in globalArgs.keySet()) {
-                val value = globalArgs[key]
-                requestCode = 31 * requestCode + (value?.hashCode() ?: 0)
-            }
-        }
+        var requestCode = globalArgs?.read { contentDeepHashCode() } ?: 0
         for (destination in destinations) {
             val destId = destination.destinationId
             requestCode = 31 * requestCode + destId
-            val arguments = destination.arguments
-            if (arguments != null) {
-                for (key in arguments.keySet()) {
-                    val value = arguments[key]
-                    requestCode = 31 * requestCode + (value?.hashCode() ?: 0)
-                }
+            val argumentsHashCode = destination.arguments?.read { contentDeepHashCode() }
+            if (argumentsHashCode != null) {
+                requestCode = 31 * requestCode + argumentsHashCode
             }
         }
         return createTaskStackBuilder()
@@ -369,7 +362,7 @@
 
                 override fun navigate(
                     destination: NavDestination,
-                    args: Bundle?,
+                    args: SavedState?,
                     navOptions: NavOptions?,
                     navigatorExtras: Extras?
                 ): NavDestination? {
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavInflater.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavInflater.kt
index f8e46da..a3e0c58 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavInflater.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavInflater.kt
@@ -20,7 +20,6 @@
 import android.content.res.Resources
 import android.content.res.TypedArray
 import android.content.res.XmlResourceParser
-import android.os.Bundle
 import android.util.AttributeSet
 import android.util.TypedValue
 import android.util.Xml
@@ -29,6 +28,9 @@
 import androidx.core.content.res.use
 import androidx.core.content.withStyledAttributes
 import androidx.navigation.common.R
+import androidx.savedstate.SavedState
+import androidx.savedstate.read
+import androidx.savedstate.savedState
 import java.io.IOException
 import org.xmlpull.v1.XmlPullParser
 import org.xmlpull.v1.XmlPullParserException
@@ -137,7 +139,7 @@
     @Throws(XmlPullParserException::class)
     private fun inflateArgumentForBundle(
         res: Resources,
-        bundle: Bundle,
+        savedState: SavedState,
         attrs: AttributeSet,
         graphResId: Int
     ) {
@@ -147,7 +149,7 @@
                     ?: throw XmlPullParserException("Arguments must have a name")
             val argument = inflateArgument(array, res, graphResId)
             if (argument.isDefaultValuePresent) {
-                argument.putDefaultValue(name, bundle)
+                argument.putDefaultValue(name, savedState)
             }
         }
     }
@@ -309,7 +311,7 @@
             builder.setPopEnterAnim(getResourceId(R.styleable.NavAction_popEnterAnim, -1))
             builder.setPopExitAnim(getResourceId(R.styleable.NavAction_popExitAnim, -1))
             action.navOptions = builder.build()
-            val args = Bundle()
+            val args = savedState()
             val innerDepth = parser.depth + 1
             var type: Int
             var depth = 0
@@ -329,7 +331,7 @@
                     inflateArgumentForBundle(res, args, attrs, graphResId)
                 }
             }
-            if (!args.isEmpty) {
+            if (!args.read { isEmpty() }) {
                 action.defaultArguments = args
             }
             dest.putAction(id, action)
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/Navigation.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/Navigation.kt
index 4597770..4cf9a03 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/Navigation.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/Navigation.kt
@@ -16,10 +16,10 @@
 package androidx.navigation
 
 import android.app.Activity
-import android.os.Bundle
 import android.view.View
 import androidx.annotation.IdRes
 import androidx.core.app.ActivityCompat
+import androidx.savedstate.SavedState
 import java.lang.ref.WeakReference
 
 /**
@@ -83,7 +83,7 @@
     @JvmOverloads
     public fun createNavigateOnClickListener(
         @IdRes resId: Int,
-        args: Bundle? = null
+        args: SavedState? = null
     ): View.OnClickListener {
         return View.OnClickListener { view -> findNavController(view).navigate(resId, args) }
     }
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/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewZoomStateTest.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewZoomStateTest.kt
new file mode 100644
index 0000000..c04af8b
--- /dev/null
+++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewZoomStateTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.pdf.view
+
+import android.graphics.Point
+import android.graphics.PointF
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class PdfViewZoomStateTest {
+
+    private lateinit var activityScenario: ActivityScenario<PdfViewTestActivity>
+
+    @Before
+    fun setup() {
+        activityScenario = ActivityScenario.launch(PdfViewTestActivity::class.java)
+    }
+
+    @After
+    fun tearDown() {
+        PdfViewTestActivity.onCreateCallback = {}
+        activityScenario.close()
+    }
+
+    private fun setupPdfView(fakePdfDocument: FakePdfDocument?) {
+        PdfViewTestActivity.onCreateCallback = { activity ->
+            val container = FrameLayout(activity)
+            val pdfView =
+                PdfView(activity).apply {
+                    pdfDocument = fakePdfDocument
+                    id = PDF_VIEW_ID
+                    minZoom = 0.5f
+                    maxZoom = 5.0f
+                }
+            container.addView(pdfView, ViewGroup.LayoutParams(PAGE_WIDTH, PAGE_HEIGHT * 2))
+            activity.setContentView(container)
+        }
+    }
+
+    @Test
+    fun testInitialZoom_fitWidth() = runTest {
+        val fakePdfDocument = FakePdfDocument(List(20) { Point(PAGE_WIDTH, PAGE_HEIGHT) })
+
+        setupPdfView(fakePdfDocument)
+
+        with(ActivityScenario.launch(PdfViewTestActivity::class.java)) {
+            fakePdfDocument.waitForLayout(untilPage = 3)
+            onView(withId(PDF_VIEW_ID)).check { view, noViewFoundException ->
+                view ?: throw noViewFoundException
+                val pdfView = view as PdfView
+                assertThat(pdfView.isInitialZoomDone).isTrue()
+                assertThat(pdfView.zoom).isWithin(0.01f).of(1.0f)
+            }
+        }
+    }
+
+    @Test
+    fun testGetDefaultZoom_fitWidth() = runTest {
+        val fakePdfDocument = FakePdfDocument(List(20) { Point(PAGE_WIDTH, PAGE_HEIGHT) })
+
+        setupPdfView(fakePdfDocument)
+        activityScenario.recreate()
+
+        activityScenario.onActivity { activity ->
+            val pdfView = activity.findViewById<PdfView>(PDF_VIEW_ID)
+            pdfView.zoom = 2.0f
+            val expectedZoom = 1.0f
+            val actualZoom = pdfView.getDefaultZoom()
+            assertThat(actualZoom).isWithin(0.01f).of(expectedZoom)
+        }
+    }
+
+    @Test
+    fun testRestoreUserZoomAndScrollPosition() = runTest {
+        val fakePdfDocument = FakePdfDocument(List(20) { Point(PAGE_WIDTH, PAGE_HEIGHT) })
+        val savedZoom = 2.5f
+        val savedScrollPosition = PointF(100f, PAGE_HEIGHT * 1f / savedZoom)
+
+        setupPdfView(fakePdfDocument)
+        activityScenario.recreate()
+
+        activityScenario.onActivity { activity ->
+            val pdfView = activity.findViewById<PdfView>(PDF_VIEW_ID)
+            pdfView.zoom = savedZoom
+            pdfView.scrollTo(
+                (savedScrollPosition.x * savedZoom - pdfView.viewportWidth / 2f).toInt(),
+                (savedScrollPosition.y * savedZoom - pdfView.viewportHeight / 2f).toInt()
+            )
+            pdfView.isInitialZoomDone = true
+        }
+
+        activityScenario.recreate()
+
+        onView(withId(PDF_VIEW_ID)).check { view, _ ->
+            view as PdfView
+            assertThat(view.zoom).isWithin(0.01f).of(savedZoom)
+            val expectedScrollX =
+                (savedScrollPosition.x * savedZoom - view.viewportWidth / 2f).toInt()
+            val expectedScrollY =
+                (savedScrollPosition.y * savedZoom - view.viewportHeight / 2f).toInt()
+            assertThat(view.scrollX).isEqualTo(expectedScrollX)
+            assertThat(view.scrollY).isEqualTo(expectedScrollY)
+        }
+    }
+}
+
+/** Arbitrary fixed ID for PdfView */
+private const val PDF_VIEW_ID = 123456789
+private const val PAGE_WIDTH = 500
+private const val PAGE_HEIGHT = 800
+
+/** The height of the viewport, minus padding */
+val PdfView.viewportHeight: Int
+    get() = bottom - top - paddingBottom - paddingTop
+
+/** The width of the viewport, minus padding */
+val PdfView.viewportWidth: Int
+    get() = right - left - paddingRight - paddingLeft
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/AccessibilityPageHelper.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/AccessibilityPageHelper.kt
index 4f23256..1850e02 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/AccessibilityPageHelper.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/AccessibilityPageHelper.kt
@@ -41,8 +41,8 @@
     private val pageManager: PageManager
 ) : ExploreByTouchHelper(pdfView) {
 
-    private var gotoLinks: MutableList<PdfPageGotoLinkContent> = mutableListOf()
-    private var urlLinks: MutableList<PdfPageLinkContent> = mutableListOf()
+    private val gotoLinks: MutableMap<Int, LinkWrapper<PdfPageGotoLinkContent>> = mutableMapOf()
+    private val urlLinks: MutableMap<Int, LinkWrapper<PdfPageLinkContent>> = mutableMapOf()
     private val totalPages = pdfView.pdfDocument?.pageCount ?: 0
     private var isLinksLoaded = false
 
@@ -56,19 +56,21 @@
             loadPageLinks()
         }
 
-        // Check if the coordinates fall within any of the gotoLinks bounds
-        gotoLinks.forEachIndexed { index, gotoLink ->
-            if (gotoLink.bounds.any { it.contains(contentX.toFloat(), contentY.toFloat()) }) {
-                return index + totalPages
+        gotoLinks.entries
+            .find { (_, wrapper) ->
+                wrapper.linkBounds.contains(contentX.toFloat(), contentY.toFloat())
             }
-        }
+            ?.let {
+                return it.key
+            }
 
-        // Check if the coordinates fall within any of the urlLinks bounds
-        urlLinks.forEachIndexed { index, urlLink ->
-            if (urlLink.bounds.any { it.contains(contentX.toFloat(), contentY.toFloat()) }) {
-                return index + totalPages + gotoLinks.size
+        urlLinks.entries
+            .find { (_, wrapper) ->
+                wrapper.linkBounds.contains(contentX.toFloat(), contentY.toFloat())
             }
-        }
+            ?.let {
+                return it.key
+            }
 
         // Check if the coordinates fall within the visible page bounds
         return (visiblePages.lower..visiblePages.upper).firstOrNull { page ->
@@ -80,14 +82,12 @@
 
     public override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>) {
         val visiblePages = pageLayoutManager.visiblePages.value
-        virtualViewIds.addAll(visiblePages.lower..visiblePages.upper)
-
         loadPageLinks()
 
-        gotoLinks.forEachIndexed { index, _ -> virtualViewIds.add(totalPages + index) }
-
-        urlLinks.forEachIndexed { index, _ ->
-            virtualViewIds.add(totalPages + gotoLinks.size + index)
+        virtualViewIds.apply {
+            addAll(visiblePages.lower..visiblePages.upper)
+            addAll(gotoLinks.keys)
+            addAll(urlLinks.keys)
         }
     }
 
@@ -97,10 +97,13 @@
     ) {
         if (!isLinksLoaded) loadPageLinks()
 
-        if (virtualViewId < totalPages) {
-            populateNodeForPage(virtualViewId, node)
-        } else {
-            populateNodeForLink(virtualViewId, node)
+        when {
+            virtualViewId < totalPages -> populateNodeForPage(virtualViewId, node)
+            else -> {
+                // Populate node for GoTo links and URL links
+                gotoLinks[virtualViewId]?.let { populateGotoLinkNode(it, node) }
+                urlLinks[virtualViewId]?.let { populateUrlLinkNode(it, node) }
+            }
         }
     }
 
@@ -134,40 +137,61 @@
         }
     }
 
-    private fun populateNodeForLink(virtualViewId: Int, node: AccessibilityNodeInfoCompat) {
-        val adjustedId = virtualViewId - totalPages
-        if (adjustedId < gotoLinks.size) {
-            populateGotoLinkNode(adjustedId, node)
-        } else {
-            populateUrlLinkNode(adjustedId - gotoLinks.size, node)
-        }
-    }
+    private fun populateGotoLinkNode(
+        linkWrapper: LinkWrapper<PdfPageGotoLinkContent>,
+        node: AccessibilityNodeInfoCompat
+    ) {
+        val bounds = scalePageBounds(linkWrapper.linkBounds, pdfView.zoom)
 
-    private fun populateGotoLinkNode(linkIndex: Int, node: AccessibilityNodeInfoCompat) {
-        val gotoLink = gotoLinks[linkIndex]
-        val bounds = scalePageBounds(gotoLink.bounds.first(), pdfView.zoom)
         node.apply {
             contentDescription =
                 pdfView.context.getString(
                     R.string.desc_goto_link,
-                    gotoLink.destination.pageNumber + 1
+                    linkWrapper.content.destination.pageNumber + 1
                 )
-            setBoundsInScreenFromBoundsInParent(node, bounds)
+            setBoundsInScreenFromBoundsInParent(this, bounds)
             isFocusable = true
         }
     }
 
-    private fun populateUrlLinkNode(linkIndex: Int, node: AccessibilityNodeInfoCompat) {
-        val urlLink = urlLinks[linkIndex]
-        val bounds = scalePageBounds(urlLink.bounds.first(), pdfView.zoom)
+    private fun populateUrlLinkNode(
+        linkWrapper: LinkWrapper<PdfPageLinkContent>,
+        node: AccessibilityNodeInfoCompat
+    ) {
+        val bounds = scalePageBounds(linkWrapper.linkBounds, pdfView.zoom)
         node.apply {
             contentDescription =
-                ExternalLinks.getDescription(urlLink.uri.toString(), pdfView.context)
+                ExternalLinks.getDescription(linkWrapper.content.uri.toString(), pdfView.context)
             setBoundsInScreenFromBoundsInParent(node, bounds)
             isFocusable = true
         }
     }
 
+    /**
+     * Calculates the adjusted bounds of a link relative to the full content of the PDF.
+     *
+     * @param pageNumber The 0-indexed page number.
+     * @param linkBounds The bounds of the link on the page.
+     * @return The adjusted bounds in the content coordinate system.
+     */
+    fun getLinkBounds(pageNumber: Int, linkBounds: RectF): RectF {
+        val pageBounds =
+            pageLayoutManager.getPageLocation(pageNumber, pdfView.getVisibleAreaInContentCoords())
+        return RectF(
+            linkBounds.left + pageBounds.left,
+            linkBounds.top + pageBounds.top,
+            linkBounds.right + pageBounds.left,
+            linkBounds.bottom + pageBounds.top
+        )
+    }
+
+    /**
+     * Scales the bounds of a page based on the current zoom level.
+     *
+     * @param bounds The original bounds to scale.
+     * @param zoom The zoom level.
+     * @return The scaled bounds as a Rect.
+     */
     @VisibleForTesting
     fun scalePageBounds(bounds: RectF, zoom: Float): Rect {
         return Rect(
@@ -178,6 +202,12 @@
         )
     }
 
+    /**
+     * Loads the links for the visible pages.
+     *
+     * This method fetches the GoTo links and URL links for the currently visible pages and stores
+     * them in the corresponding maps.
+     */
     fun loadPageLinks() {
         val visiblePages = pageLayoutManager.visiblePages.value
 
@@ -185,10 +215,20 @@
         gotoLinks.clear()
         urlLinks.clear()
 
+        var cumulativeId = totalPages
+
         (visiblePages.lower..visiblePages.upper).forEach { pageIndex ->
             pageManager.pages[pageIndex]?.links?.let { links ->
-                links.gotoLinks.let { gotoLinks.addAll(it) }
-                links.externalLinks.let { urlLinks.addAll(it) }
+                links.gotoLinks.forEach { link ->
+                    gotoLinks[cumulativeId] =
+                        LinkWrapper(pageIndex, link, getLinkBounds(pageIndex, link.bounds.first()))
+                    cumulativeId++
+                }
+                links.externalLinks.forEach { link ->
+                    urlLinks[cumulativeId] =
+                        LinkWrapper(pageIndex, link, getLinkBounds(pageIndex, link.bounds.first()))
+                    cumulativeId++
+                }
             }
         }
         isLinksLoaded = true
@@ -243,3 +283,13 @@
         }
     }
 }
+
+/**
+ * A wrapper class for links in the PDF document.
+ *
+ * @param T The type of link content (GotoLink or URL link).
+ * @param pageNumber The 0-indexed page number where the link is located.
+ * @param content The link's content (GotoLink or URL link).
+ * @param linkBounds The link's bounds in the full PDF's content coordinates.
+ */
+private data class LinkWrapper<T>(val pageNumber: Int, val content: T, val linkBounds: RectF)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
index 0c1973b..2621d74 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
@@ -215,6 +215,7 @@
     private var awaitingFirstLayout: Boolean = true
     private var scrollPositionToRestore: PointF? = null
     private var zoomToRestore: Float? = null
+    @VisibleForTesting internal var isInitialZoomDone: Boolean = false
     /**
      * The width of the PdfView before the last layout change (e.g., before rotation). Used to
      * preserve the zoom level when the device is rotated.
@@ -521,6 +522,7 @@
         val superState = super.onSaveInstanceState()
         val state = PdfViewSavedState(superState)
         state.zoom = zoom
+        state.isInitialZoomDone = isInitialZoomDone
         state.viewWidth = width
         state.contentCenterX = toContentX(viewportWidth.toFloat() / 2f)
         state.contentCenterY = toContentY(viewportHeight.toFloat() / 2f)
@@ -605,7 +607,8 @@
         }
     }
 
-    private fun getDefaultZoom(): Float {
+    @VisibleForTesting
+    internal fun getDefaultZoom(): Float {
         if (contentWidth == 0 || viewportWidth == 0) return DEFAULT_INIT_ZOOM
         val widthZoom = viewportWidth.toFloat() / contentWidth
         return MathUtils.clamp(widthZoom, minZoom, maxZoom)
@@ -649,6 +652,7 @@
             scrollPositionToRestore = positionToRestore
             zoomToRestore = localStateToRestore.zoom
             oldWidth = localStateToRestore.viewWidth
+            isInitialZoomDone = localStateToRestore.isInitialZoomDone
         } else {
             scrollToRestoredPosition(positionToRestore, localStateToRestore.zoom)
         }
@@ -927,7 +931,10 @@
         // centering if it's needed. It doesn't override any restored state because we're scrolling
         // to the current scroll position.
         if (pageNum == 0) {
-            this.zoom = getDefaultZoom()
+            if (!isInitialZoomDone) {
+                this.zoom = getDefaultZoom()
+                isInitialZoomDone = true
+            }
             scrollTo(scrollX, scrollY)
         }
 
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfViewSavedState.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfViewSavedState.kt
index c0c5047..dd0ab91 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfViewSavedState.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfViewSavedState.kt
@@ -30,6 +30,7 @@
     var zoom: Float = 1F
     var documentUri: Uri? = null
     var paginationModel: PaginationModel? = null
+    var isInitialZoomDone: Boolean = false
     /**
      * The width of the PdfView before the last layout change (e.g., before rotation). Used to
      * preserve the zoom level when the device is rotated.
diff --git a/performance/performance-annotation/README.md b/performance/performance-annotation/README.md
index b37c607..2ab0c3d 100644
--- a/performance/performance-annotation/README.md
+++ b/performance/performance-annotation/README.md
@@ -1 +1 @@
-The annotation defined in this library only affects Android.
+This library is a **compile-time** only dependency.
diff --git a/performance/performance-annotation/src/androidMain/kotlin/dalvik/annotation/optimization/NeverInline.android.kt b/performance/performance-annotation/src/androidMain/kotlin/dalvik/annotation/optimization/NeverInline.android.kt
index a44f0ff..50e22dc 100644
--- a/performance/performance-annotation/src/androidMain/kotlin/dalvik/annotation/optimization/NeverInline.android.kt
+++ b/performance/performance-annotation/src/androidMain/kotlin/dalvik/annotation/optimization/NeverInline.android.kt
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-@file:Suppress("RedundantVisibilityModifier")
-
 package dalvik.annotation.optimization
 
 @Retention(AnnotationRetention.BINARY)
 @Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION)
-public actual annotation class NeverInline
+public actual annotation class NeverInline()
diff --git a/performance/performance-annotation/src/commonMain/kotlin/dalvik/annotation/optimization/NeverInline.kt b/performance/performance-annotation/src/commonMain/kotlin/dalvik/annotation/optimization/NeverInline.kt
index 99f736b..89cbfe3 100644
--- a/performance/performance-annotation/src/commonMain/kotlin/dalvik/annotation/optimization/NeverInline.kt
+++ b/performance/performance-annotation/src/commonMain/kotlin/dalvik/annotation/optimization/NeverInline.kt
@@ -15,7 +15,6 @@
  */
 
 @file:OptIn(ExperimentalMultiplatform::class)
-@file:Suppress("RedundantVisibilityModifier")
 
 package dalvik.annotation.optimization
 
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/compose/runtime/runtime/src/nonAndroidMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.nonAndroid.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt
similarity index 63%
rename from compose/runtime/runtime/src/nonAndroidMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.nonAndroid.kt
rename to privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt
index 2e2c834..27b1299 100644
--- a/compose/runtime/runtime/src/nonAndroidMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.nonAndroid.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt
@@ -13,14 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package androidx.privacysandbox.ui.client.test
 
-@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+import androidx.activity.ComponentActivity
 
-package androidx.compose.runtime.collection
-
-internal actual inline fun <T> Array<out T>.fastCopyInto(
-    destination: Array<T>,
-    destinationOffset: Int,
-    startIndex: Int,
-    endIndex: Int
-): Array<T> = this.copyInto(destination, destinationOffset, startIndex, endIndex)
+class UiLibComposeActivity : ComponentActivity() {}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/ClientDelegatingAdapter.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/ClientDelegatingAdapter.kt
index 2901fe9..404139d 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/ClientDelegatingAdapter.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/ClientDelegatingAdapter.kt
@@ -21,13 +21,13 @@
 import android.os.IBinder
 import androidx.annotation.GuardedBy
 import androidx.core.util.Consumer
-import androidx.privacysandbox.ui.client.RemoteCallManager.tryToCallRemoteObject
 import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory.createFromCoreLibInfo
 import androidx.privacysandbox.ui.client.view.RefreshableSessionClient
 import androidx.privacysandbox.ui.core.IDelegateChangeListener
 import androidx.privacysandbox.ui.core.IDelegatingSandboxedUiAdapter
 import androidx.privacysandbox.ui.core.IDelegatorCallback
 import androidx.privacysandbox.ui.core.ISessionRefreshCallback
+import androidx.privacysandbox.ui.core.RemoteCallManager.tryToCallRemoteObject
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import java.util.concurrent.Executor
 import kotlin.coroutines.resume
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
index e4a4f5f..e80268a 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -30,14 +30,14 @@
 import android.view.View
 import android.window.SurfaceSyncGroup
 import androidx.annotation.RequiresApi
-import androidx.privacysandbox.ui.client.RemoteCallManager.addBinderDeathListener
-import androidx.privacysandbox.ui.client.RemoteCallManager.closeRemoteSession
-import androidx.privacysandbox.ui.client.RemoteCallManager.tryToCallRemoteObject
 import androidx.privacysandbox.ui.core.IDelegatingSandboxedUiAdapter
 import androidx.privacysandbox.ui.core.IRemoteSessionClient
 import androidx.privacysandbox.ui.core.IRemoteSessionController
 import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
 import androidx.privacysandbox.ui.core.ProtocolConstants
+import androidx.privacysandbox.ui.core.RemoteCallManager.addBinderDeathListener
+import androidx.privacysandbox.ui.core.RemoteCallManager.closeRemoteSession
+import androidx.privacysandbox.ui.core.RemoteCallManager.tryToCallRemoteObject
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import java.lang.reflect.InvocationHandler
 import java.lang.reflect.Method
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/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/RemoteCallManager.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/RemoteCallManager.kt
similarity index 93%
rename from privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/RemoteCallManager.kt
rename to privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/RemoteCallManager.kt
index 6570440..3d75a93 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/RemoteCallManager.kt
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/RemoteCallManager.kt
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.ui.client
+package androidx.privacysandbox.ui.core
 
 import android.os.IBinder
 import android.os.RemoteException
 import android.util.Log
 import androidx.annotation.RestrictTo
-import androidx.privacysandbox.ui.core.IRemoteSessionController
 
 /** Utility class for remote objects called by the UI library adapter factories. */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
index 3dd9727..697e3c8 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
@@ -40,6 +40,7 @@
 import androidx.privacysandbox.ui.core.IRemoteSessionController
 import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
 import androidx.privacysandbox.ui.core.ProtocolConstants
+import androidx.privacysandbox.ui.core.RemoteCallManager.tryToCallRemoteObject
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import androidx.privacysandbox.ui.core.SessionObserver
 import androidx.privacysandbox.ui.core.SessionObserverContext
@@ -78,20 +79,22 @@
 
         override suspend fun onDelegateChanged(delegate: Bundle) {
             suspendCancellableCoroutine { continuation ->
-                binder.onDelegateChanged(
-                    delegate,
-                    object : IDelegatorCallback.Stub() {
-                        override fun onDelegateChangeResult(success: Boolean) {
-                            if (success) {
-                                continuation.resume(Unit)
-                            } else {
-                                continuation.resumeWithException(
-                                    IllegalStateException("Client failed to switch")
-                                )
+                tryToCallRemoteObject(binder) {
+                    onDelegateChanged(
+                        delegate,
+                        object : IDelegatorCallback.Stub() {
+                            override fun onDelegateChangeResult(success: Boolean) {
+                                if (success) {
+                                    continuation.resume(Unit)
+                                } else {
+                                    continuation.resumeWithException(
+                                        IllegalStateException("Client failed to switch")
+                                    )
+                                }
                             }
                         }
-                    }
-                )
+                    )
+                }
             }
         }
     }
@@ -154,7 +157,9 @@
         remoteSessionClient: IRemoteSessionClient
     ) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
-            remoteSessionClient.onRemoteSessionError("openRemoteSession() requires API34+")
+            tryToCallRemoteObject(remoteSessionClient) {
+                onRemoteSessionError("openRemoteSession() requires API34+")
+            }
             return
         }
 
@@ -177,7 +182,11 @@
                             )
                         },
                         clientInit = { it.initialize(initialWidth, initialHeight) },
-                        errorHandler = { remoteSessionClient.onRemoteSessionError(it.message) }
+                        errorHandler = {
+                            tryToCallRemoteObject(remoteSessionClient) {
+                                onRemoteSessionError(it.message)
+                            }
+                        }
                     )
 
                 openSessionInternal(
@@ -192,7 +201,9 @@
 
                 deferredClient.preloadClient()
             } catch (exception: Throwable) {
-                remoteSessionClient.onRemoteSessionError(exception.message)
+                tryToCallRemoteObject(remoteSessionClient) {
+                    onRemoteSessionError(exception.message)
+                }
             }
         }
     }
@@ -280,27 +291,31 @@
         }
 
         override fun onSessionError(throwable: Throwable) {
-            remoteSessionClient.onRemoteSessionError(throwable.message)
+            tryToCallRemoteObject(remoteSessionClient) { onRemoteSessionError(throwable.message) }
         }
 
         override fun onResizeRequested(width: Int, height: Int) {
-            remoteSessionClient.onResizeRequested(width, height)
+            tryToCallRemoteObject(remoteSessionClient) { onResizeRequested(width, height) }
         }
 
         private fun sendRemoteSessionOpened(session: SandboxedUiAdapter.Session) {
             val surfacePackage = surfaceControlViewHost.surfacePackage
             val remoteSessionController = RemoteSessionController(surfaceControlViewHost, session)
-            remoteSessionClient.onRemoteSessionOpened(
-                surfacePackage,
-                remoteSessionController,
-                isZOrderOnTop,
-                session.signalOptions.isNotEmpty()
-            )
+            tryToCallRemoteObject(remoteSessionClient) {
+                onRemoteSessionOpened(
+                    surfacePackage,
+                    remoteSessionController,
+                    isZOrderOnTop,
+                    session.signalOptions.isNotEmpty()
+                )
+            }
         }
 
         private fun sendSurfacePackage() {
             if (surfaceControlViewHost.surfacePackage != null) {
-                remoteSessionClient.onSessionUiFetched(surfaceControlViewHost.surfacePackage)
+                tryToCallRemoteObject(remoteSessionClient) {
+                    onSessionUiFetched(surfaceControlViewHost.surfacePackage)
+                }
             }
         }
 
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 1cf5db4..e8eed4f 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -32,12 +32,14 @@
 }
 
 androidXMultiplatform {
+    js()
     jvm() {
         withJava()
     }
     mac()
     linux()
     ios()
+    wasmJs()
 
     defaultPlatform(PlatformIdentifier.JVM)
 
@@ -45,7 +47,7 @@
         commonMain {
             dependencies {
                 api(libs.kotlinStdlib)
-                api("androidx.annotation:annotation:1.8.1")
+                api("androidx.annotation:annotation:1.9.1")
             }
         }
 
diff --git a/room/room-common/src/jsMain/kotlin/androidx/room/ConstructedBy.js.kt b/room/room-common/src/jsMain/kotlin/androidx/room/ConstructedBy.js.kt
new file mode 100644
index 0000000..b43bd1e
--- /dev/null
+++ b/room/room-common/src/jsMain/kotlin/androidx/room/ConstructedBy.js.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room
+
+import kotlin.reflect.AssociatedObjectKey
+import kotlin.reflect.ExperimentalAssociatedObjects
+import kotlin.reflect.KClass
+
+/**
+ * Defines the [androidx.room.RoomDatabaseConstructor] that will instantiate the Room generated
+ * implementation of the annotated [Database].
+ *
+ * A [androidx.room.RoomDatabase] database definition must be annotated with this annotation if it
+ * is located in a common source set on a Kotlin Multiplatform project such that at runtime the
+ * implementation generated by the annotation processor can be used. The [value] must be an 'expect
+ * object' that implements [androidx.room.RoomDatabaseConstructor].
+ *
+ * Example usage:
+ * ```
+ * @Database(version = 1, entities = [Song::class, Album::class])
+ * @ConstructedBy(MusicDatabaseConstructor::class)
+ * abstract class MusicDatabase : RoomDatabase
+ *
+ * expect object MusicDatabaseConstructor : RoomDatabaseConstructor<MusicDatabase>
+ * ```
+ *
+ * @see androidx.room.RoomDatabaseConstructor
+ */
+@OptIn(ExperimentalAssociatedObjects::class)
+@AssociatedObjectKey
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+actual annotation class ConstructedBy(
+    /**
+     * The 'expect' declaration of an 'object' that implements
+     * [androidx.room.RoomDatabaseConstructor] and is able to instantiate a
+     * [androidx.room.RoomDatabase].
+     */
+    actual val value: KClass<*>
+)
diff --git a/room/room-common/src/wasmJsMain/kotlin/androidx/room/ConstructedBy.wasmJs.kt b/room/room-common/src/wasmJsMain/kotlin/androidx/room/ConstructedBy.wasmJs.kt
new file mode 100644
index 0000000..b43bd1e
--- /dev/null
+++ b/room/room-common/src/wasmJsMain/kotlin/androidx/room/ConstructedBy.wasmJs.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room
+
+import kotlin.reflect.AssociatedObjectKey
+import kotlin.reflect.ExperimentalAssociatedObjects
+import kotlin.reflect.KClass
+
+/**
+ * Defines the [androidx.room.RoomDatabaseConstructor] that will instantiate the Room generated
+ * implementation of the annotated [Database].
+ *
+ * A [androidx.room.RoomDatabase] database definition must be annotated with this annotation if it
+ * is located in a common source set on a Kotlin Multiplatform project such that at runtime the
+ * implementation generated by the annotation processor can be used. The [value] must be an 'expect
+ * object' that implements [androidx.room.RoomDatabaseConstructor].
+ *
+ * Example usage:
+ * ```
+ * @Database(version = 1, entities = [Song::class, Album::class])
+ * @ConstructedBy(MusicDatabaseConstructor::class)
+ * abstract class MusicDatabase : RoomDatabase
+ *
+ * expect object MusicDatabaseConstructor : RoomDatabaseConstructor<MusicDatabase>
+ * ```
+ *
+ * @see androidx.room.RoomDatabaseConstructor
+ */
+@OptIn(ExperimentalAssociatedObjects::class)
+@AssociatedObjectKey
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+actual annotation class ConstructedBy(
+    /**
+     * The 'expect' declaration of an 'object' that implements
+     * [androidx.room.RoomDatabaseConstructor] and is able to instantiate a
+     * [androidx.room.RoomDatabase].
+     */
+    actual val value: KClass<*>
+)
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/Ksp2Compilation.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/Ksp2Compilation.kt
index 4764bed..3d7aaa3 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/Ksp2Compilation.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/Ksp2Compilation.kt
@@ -62,7 +62,7 @@
                 diagnostics = kspDiagnostics.messages,
                 sourceSets = arguments.sourceSets + generatedSources
             )
-        val outputResources = workingDir.resolve(RESOURCES_OUT_FOLDER_NAME)
+        val outputResources = workingDir.resolve(RESOURCE_OUT_FOLDER_NAME)
         val outputClasspath = listOf(workingDir.resolve(CLASS_OUT_FOLDER_NAME))
         val generatedResources =
             outputResources
@@ -168,6 +168,5 @@
         private const val RESOURCE_OUT_FOLDER_NAME = "ksp-resource-out"
         private const val CACHE_FOLDER_NAME = "ksp-cache"
         private const val CLASS_OUT_FOLDER_NAME = "class-out"
-        private const val RESOURCES_OUT_FOLDER_NAME = "ksp-compiler/resourceOutputDir"
     }
 }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
index 1753a3e..ab518b2 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
@@ -169,19 +169,22 @@
 
     @Test
     fun writeResource() {
+        val logFileName = "test.log"
+        val serviceFileName = "META-INF/services/com.test.Foo"
         runKspTest(sources = emptyList()) { invocation ->
             invocation.processingEnv.filer
-                .writeResource(filePath = Path("test.log"), originatingElements = emptyList())
+                .writeResource(filePath = Path(logFileName), originatingElements = emptyList())
                 .bufferedWriter(Charsets.UTF_8)
                 .use { it.write("Hello!") }
             invocation.processingEnv.filer
-                .writeResource(
-                    filePath = Path("META-INF/services/com.test.Foo"),
-                    originatingElements = emptyList()
-                )
+                .writeResource(filePath = Path(serviceFileName), originatingElements = emptyList())
                 .bufferedWriter(Charsets.UTF_8)
                 .use { it.write("Not a real service...") }
-            invocation.assertCompilationResult { hasNoWarnings() }
+            invocation.assertCompilationResult {
+                generatedResourceFileWithPath(logFileName)
+                generatedResourceFileWithPath(serviceFileName)
+                hasNoWarnings()
+            }
         }
     }
 
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 af9e0bd..7249870 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();
@@ -240,7 +240,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 6085db8..c5c3df3 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();
@@ -265,7 +265,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/security/security-state-provider/src/main/java/androidx/security/state/provider/UpdateInfoManager.kt b/security/security-state-provider/src/main/java/androidx/security/state/provider/UpdateInfoManager.kt
index f04ac2ff..b0e695b 100644
--- a/security/security-state-provider/src/main/java/androidx/security/state/provider/UpdateInfoManager.kt
+++ b/security/security-state-provider/src/main/java/androidx/security/state/provider/UpdateInfoManager.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import androidx.security.state.SecurityPatchState
+import androidx.security.state.SecurityPatchState.Companion.getComponentSecurityPatchLevel
 import kotlinx.serialization.json.Json
 
 /**
@@ -93,11 +94,7 @@
                 // Ignore unknown components.
                 return@forEach
             }
-            val updateSpl =
-                securityState.getComponentSecurityPatchLevel(
-                    component,
-                    updateInfo.securityPatchLevel
-                )
+            val updateSpl = getComponentSecurityPatchLevel(component, updateInfo.securityPatchLevel)
 
             if (updateSpl <= currentSpl) {
                 val key = getKeyForUpdateInfo(updateInfo)
diff --git a/security/security-state-provider/src/test/java/androidx/security/state/provider/UpdateInfoManagerTest.kt b/security/security-state-provider/src/test/java/androidx/security/state/provider/UpdateInfoManagerTest.kt
index 81b6111..a87a5d7 100644
--- a/security/security-state-provider/src/test/java/androidx/security/state/provider/UpdateInfoManagerTest.kt
+++ b/security/security-state-provider/src/test/java/androidx/security/state/provider/UpdateInfoManagerTest.kt
@@ -33,20 +33,13 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.`when`
 import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 
 @RunWith(AndroidJUnit4::class)
 class UpdateInfoManagerTest {
 
     private lateinit var manager: UpdateInfoManager
-    private val mockSpl = SecurityPatchState.DateBasedSecurityPatchLevel(2022, 1, 1)
-    private val mockSecurityState: SecurityPatchState =
-        mock<SecurityPatchState> {
-            on {
-                getComponentSecurityPatchLevel(eq(COMPONENT_SYSTEM), Mockito.anyString())
-            } doReturn mockSpl
-        }
+    private val mockSecurityState: SecurityPatchState = mock<SecurityPatchState>()
     @SuppressLint("NewApi")
     private val publishedDate = Date.from(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC))
     private val updateInfo =
diff --git a/security/security-state/api/current.txt b/security/security-state/api/current.txt
index 510499b..8cae039 100644
--- a/security/security-state/api/current.txt
+++ b/security/security-state/api/current.txt
@@ -3,16 +3,17 @@
 
   public class SecurityPatchState {
     ctor public SecurityPatchState(android.content.Context context);
-    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModules);
-    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModules, optional androidx.security.state.SecurityStateManager? customSecurityStateManager);
+    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModulePackageNames);
+    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModulePackageNames, optional androidx.security.state.SecurityStateManagerCompat? customSecurityStateManagerCompat);
+    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModulePackageNames, optional androidx.security.state.SecurityStateManagerCompat? customSecurityStateManagerCompat, optional String? vulnerabilityReportJsonString);
     method public final boolean areCvesPatched(java.util.List<java.lang.String> cveList);
-    method public androidx.security.state.SecurityPatchState.SecurityPatchLevel getComponentSecurityPatchLevel(String component, String securityPatchLevel);
+    method public static final androidx.security.state.SecurityPatchState.SecurityPatchLevel getComponentSecurityPatchLevel(String component, String securityPatchLevel);
     method public androidx.security.state.SecurityPatchState.SecurityPatchLevel getDeviceSecurityPatchLevel(String component);
     method public java.util.Map<androidx.security.state.SecurityPatchState.Severity,java.util.Set<java.lang.String>> getPatchedCves(String component, androidx.security.state.SecurityPatchState.SecurityPatchLevel spl);
     method public java.util.List<androidx.security.state.SecurityPatchState.SecurityPatchLevel> getPublishedSecurityPatchLevel(String component);
-    method @RequiresApi(26) public final android.net.Uri getVulnerabilityReportUrl(android.net.Uri serverUrl);
+    method @RequiresApi(26) public static final android.net.Uri getVulnerabilityReportUrl(optional android.net.Uri serverUrl);
     method public final boolean isDeviceFullyUpdated();
-    method public final void loadVulnerabilityReport(String jsonString);
+    method @WorkerThread public final void loadVulnerabilityReport(String jsonString);
     field public static final String COMPONENT_KERNEL = "KERNEL";
     field public static final String COMPONENT_SYSTEM = "SYSTEM";
     field public static final String COMPONENT_SYSTEM_MODULES = "SYSTEM_MODULES";
@@ -22,6 +23,8 @@
   }
 
   public static final class SecurityPatchState.Companion {
+    method public androidx.security.state.SecurityPatchState.SecurityPatchLevel getComponentSecurityPatchLevel(String component, String securityPatchLevel);
+    method @RequiresApi(26) public android.net.Uri getVulnerabilityReportUrl(optional android.net.Uri serverUrl);
     property public static final String COMPONENT_KERNEL;
     property public static final String COMPONENT_SYSTEM;
     property public static final String COMPONENT_SYSTEM_MODULES;
@@ -75,16 +78,16 @@
     method public androidx.security.state.SecurityPatchState.VersionedSecurityPatchLevel fromString(String value);
   }
 
-  public class SecurityStateManager {
-    ctor public SecurityStateManager(android.content.Context context);
-    method public android.os.Bundle getGlobalSecurityState(optional String? moduleMetadataProvider);
-    field public static final androidx.security.state.SecurityStateManager.Companion Companion;
+  public class SecurityStateManagerCompat {
+    ctor public SecurityStateManagerCompat(android.content.Context context);
+    method public android.os.Bundle getGlobalSecurityState(optional String moduleMetadataProviderPackageName);
+    field public static final androidx.security.state.SecurityStateManagerCompat.Companion Companion;
     field public static final String KEY_KERNEL_VERSION = "kernel_version";
     field public static final String KEY_SYSTEM_SPL = "system_spl";
     field public static final String KEY_VENDOR_SPL = "vendor_spl";
   }
 
-  public static final class SecurityStateManager.Companion {
+  public static final class SecurityStateManagerCompat.Companion {
     property public static final String KEY_KERNEL_VERSION;
     property public static final String KEY_SYSTEM_SPL;
     property public static final String KEY_VENDOR_SPL;
diff --git a/security/security-state/api/restricted_current.txt b/security/security-state/api/restricted_current.txt
index 510499b..8cae039 100644
--- a/security/security-state/api/restricted_current.txt
+++ b/security/security-state/api/restricted_current.txt
@@ -3,16 +3,17 @@
 
   public class SecurityPatchState {
     ctor public SecurityPatchState(android.content.Context context);
-    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModules);
-    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModules, optional androidx.security.state.SecurityStateManager? customSecurityStateManager);
+    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModulePackageNames);
+    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModulePackageNames, optional androidx.security.state.SecurityStateManagerCompat? customSecurityStateManagerCompat);
+    ctor public SecurityPatchState(android.content.Context context, optional java.util.List<java.lang.String> systemModulePackageNames, optional androidx.security.state.SecurityStateManagerCompat? customSecurityStateManagerCompat, optional String? vulnerabilityReportJsonString);
     method public final boolean areCvesPatched(java.util.List<java.lang.String> cveList);
-    method public androidx.security.state.SecurityPatchState.SecurityPatchLevel getComponentSecurityPatchLevel(String component, String securityPatchLevel);
+    method public static final androidx.security.state.SecurityPatchState.SecurityPatchLevel getComponentSecurityPatchLevel(String component, String securityPatchLevel);
     method public androidx.security.state.SecurityPatchState.SecurityPatchLevel getDeviceSecurityPatchLevel(String component);
     method public java.util.Map<androidx.security.state.SecurityPatchState.Severity,java.util.Set<java.lang.String>> getPatchedCves(String component, androidx.security.state.SecurityPatchState.SecurityPatchLevel spl);
     method public java.util.List<androidx.security.state.SecurityPatchState.SecurityPatchLevel> getPublishedSecurityPatchLevel(String component);
-    method @RequiresApi(26) public final android.net.Uri getVulnerabilityReportUrl(android.net.Uri serverUrl);
+    method @RequiresApi(26) public static final android.net.Uri getVulnerabilityReportUrl(optional android.net.Uri serverUrl);
     method public final boolean isDeviceFullyUpdated();
-    method public final void loadVulnerabilityReport(String jsonString);
+    method @WorkerThread public final void loadVulnerabilityReport(String jsonString);
     field public static final String COMPONENT_KERNEL = "KERNEL";
     field public static final String COMPONENT_SYSTEM = "SYSTEM";
     field public static final String COMPONENT_SYSTEM_MODULES = "SYSTEM_MODULES";
@@ -22,6 +23,8 @@
   }
 
   public static final class SecurityPatchState.Companion {
+    method public androidx.security.state.SecurityPatchState.SecurityPatchLevel getComponentSecurityPatchLevel(String component, String securityPatchLevel);
+    method @RequiresApi(26) public android.net.Uri getVulnerabilityReportUrl(optional android.net.Uri serverUrl);
     property public static final String COMPONENT_KERNEL;
     property public static final String COMPONENT_SYSTEM;
     property public static final String COMPONENT_SYSTEM_MODULES;
@@ -75,16 +78,16 @@
     method public androidx.security.state.SecurityPatchState.VersionedSecurityPatchLevel fromString(String value);
   }
 
-  public class SecurityStateManager {
-    ctor public SecurityStateManager(android.content.Context context);
-    method public android.os.Bundle getGlobalSecurityState(optional String? moduleMetadataProvider);
-    field public static final androidx.security.state.SecurityStateManager.Companion Companion;
+  public class SecurityStateManagerCompat {
+    ctor public SecurityStateManagerCompat(android.content.Context context);
+    method public android.os.Bundle getGlobalSecurityState(optional String moduleMetadataProviderPackageName);
+    field public static final androidx.security.state.SecurityStateManagerCompat.Companion Companion;
     field public static final String KEY_KERNEL_VERSION = "kernel_version";
     field public static final String KEY_SYSTEM_SPL = "system_spl";
     field public static final String KEY_VENDOR_SPL = "vendor_spl";
   }
 
-  public static final class SecurityStateManager.Companion {
+  public static final class SecurityStateManagerCompat.Companion {
     property public static final String KEY_KERNEL_VERSION;
     property public static final String KEY_SYSTEM_SPL;
     property public static final String KEY_VENDOR_SPL;
diff --git a/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt b/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerCompatTest.kt
similarity index 88%
rename from security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt
rename to security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerCompatTest.kt
index 7d2ddfa..3c72447 100644
--- a/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt
+++ b/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerCompatTest.kt
@@ -32,14 +32,14 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-class SecurityStateManagerTest {
+class SecurityStateManagerCompatTest {
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
-    private lateinit var securityStateManager: SecurityStateManager
+    private lateinit var securityStateManagerCompat: SecurityStateManagerCompat
 
     @Before
     fun setup() {
-        securityStateManager = SecurityStateManager(context)
+        securityStateManagerCompat = SecurityStateManagerCompat(context)
     }
 
     /** Returns `true` if [date] is in the format "YYYY-MM-DD". */
@@ -85,7 +85,7 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     @Test
     fun testGetGlobalSecurityState_sdkAbove29() {
-        val bundle = securityStateManager.getGlobalSecurityState()
+        val bundle = securityStateManagerCompat.getGlobalSecurityState()
         assertTrue(matchesDateFormat(bundle.getString("system_spl")!!))
         assertTrue(matchesKernelFormat(bundle.getString("kernel_version")!!))
         assertTrue(containsModuleMetadataPackage(bundle))
@@ -95,7 +95,7 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O, maxSdkVersion = Build.VERSION_CODES.P)
     @Test
     fun testGetGlobalSecurityState_sdkAbove25Below29_doesNotContainModuleMetadata() {
-        val bundle = securityStateManager.getGlobalSecurityState()
+        val bundle = securityStateManagerCompat.getGlobalSecurityState()
         assertTrue(matchesDateFormat(bundle.getString("system_spl")!!))
         assertTrue(matchesKernelFormat(bundle.getString("kernel_version")!!))
         assertTrue(containsWebViewPackage(bundle))
@@ -105,7 +105,7 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M, maxSdkVersion = Build.VERSION_CODES.N_MR1)
     @Test
     fun testGetGlobalSecurityState_sdkAbove22Below26_doesNotContainModuleMetadataOrWebView() {
-        val bundle = securityStateManager.getGlobalSecurityState()
+        val bundle = securityStateManagerCompat.getGlobalSecurityState()
         assertTrue(matchesDateFormat(bundle.getString("system_spl")!!))
         assertTrue(matchesKernelFormat(bundle.getString("kernel_version")!!))
         assertFalse(containsModuleMetadataPackage(bundle))
@@ -118,7 +118,7 @@
     )
     @Test
     fun testGetGlobalSecurityState_sdkBelow23_containsOnlyKernel() {
-        val bundle = securityStateManager.getGlobalSecurityState()
+        val bundle = securityStateManagerCompat.getGlobalSecurityState()
         assertTrue(matchesKernelFormat(bundle.getString("kernel_version")!!))
         assertFalse(bundle.containsKey("system_spl"))
         assertFalse(bundle.containsKey("vendor_spl"))
@@ -130,7 +130,7 @@
     @Test
     fun testGetGlobalSecurityState_whenVendorIsEnabled_containsVendorSpl() {
         SecurityPatchState.Companion.USE_VENDOR_SPL = true
-        val bundle = securityStateManager.getGlobalSecurityState()
+        val bundle = securityStateManagerCompat.getGlobalSecurityState()
         assertTrue(bundle.containsKey("vendor_spl"))
     }
 
@@ -138,7 +138,7 @@
     @Test
     fun testGetGlobalSecurityState_whenVendorIsDisabled_doesNotContainVendorSpl() {
         SecurityPatchState.Companion.USE_VENDOR_SPL = false
-        val bundle = securityStateManager.getGlobalSecurityState()
+        val bundle = securityStateManagerCompat.getGlobalSecurityState()
         assertFalse(bundle.containsKey("vendor_spl"))
     }
 
@@ -149,7 +149,7 @@
             return // Skip this test on non-Google devices.
         }
         val bundle =
-            securityStateManager.getGlobalSecurityState("com.google.android.modulemetadata")
+            securityStateManagerCompat.getGlobalSecurityState("com.google.android.modulemetadata")
         DateBasedSecurityPatchLevel.fromString(
             bundle.getString("com.google.android.modulemetadata")!!
         )
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
index 455fe21..bcc3f13 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
@@ -19,11 +19,13 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.net.Uri
+import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.annotation.StringDef
-import androidx.security.state.SecurityStateManager.Companion.KEY_KERNEL_VERSION
-import androidx.security.state.SecurityStateManager.Companion.KEY_SYSTEM_SPL
-import androidx.security.state.SecurityStateManager.Companion.KEY_VENDOR_SPL
+import androidx.annotation.WorkerThread
+import androidx.security.state.SecurityStateManagerCompat.Companion.KEY_KERNEL_VERSION
+import androidx.security.state.SecurityStateManagerCompat.Companion.KEY_SYSTEM_SPL
+import androidx.security.state.SecurityStateManagerCompat.Companion.KEY_VENDOR_SPL
 import java.text.ParseException
 import java.text.SimpleDateFormat
 import java.util.Calendar
@@ -48,27 +50,48 @@
  * The class uses a combination of local data storage and external data fetching to maintain and
  * update security states.
  *
+ * Recommended pattern of usage:
+ * - call [getVulnerabilityReportUrl] and make a request to download the JSON file containing
+ *   vulnerability report data
+ * - create SecurityPatchState object, passing in the downloaded JSON as a [String]
+ * - call [getPublishedSecurityPatchLevel] or other APIs
+ *
  * @param context Application context used for accessing shared preferences, resources, and other
  *   context-dependent features.
- * @param systemModules A list of system module package names, defaults to Google provided system
- *   modules if none are provided. The first module on the list must be the system modules metadata
- *   provider package.
- * @param customSecurityStateManager An optional custom manager for obtaining security state
+ * @param systemModulePackageNames A list of system module package names, defaults to Google
+ *   provided system modules if none are provided. The first module on the list must be the system
+ *   modules metadata provider package.
+ * @param customSecurityStateManagerCompat An optional custom manager for obtaining security state
  *   information. If null, a default manager is instantiated.
+ * @param vulnerabilityReportJsonString A JSON string containing vulnerability data to initialize a
+ *   [VulnerabilityReport] object.
+ *
+ *   If you only care about the Device SPL, this parameter is optional. If you need access to
+ *   Published SPL and Available SPL, you must provide this JSON string, either here in the
+ *   constructor, or later using [loadVulnerabilityReport].
+ *
  * @constructor Creates an instance of SecurityPatchState.
  */
 public open class SecurityPatchState
 @JvmOverloads
 constructor(
     private val context: Context,
-    private val systemModules: List<String> = listOf(),
-    private val customSecurityStateManager: SecurityStateManager? = null
+    private val systemModulePackageNames: List<String> = DEFAULT_SYSTEM_MODULES,
+    private val customSecurityStateManagerCompat: SecurityStateManagerCompat? = null,
+    vulnerabilityReportJsonString: String? = null
 ) {
-    private val securityStateManager =
-        customSecurityStateManager ?: SecurityStateManager(context = context)
+    init {
+        if (vulnerabilityReportJsonString != null) {
+            loadVulnerabilityReport(vulnerabilityReportJsonString)
+        }
+    }
+
+    private val securityStateManagerCompat =
+        customSecurityStateManagerCompat ?: SecurityStateManagerCompat(context = context)
     private var vulnerabilityReport: VulnerabilityReport? = null
 
     public companion object {
+        /** Default list of Android Mainline system modules. */
         @JvmField
         public val DEFAULT_SYSTEM_MODULES: List<String> =
             listOf(
@@ -102,6 +125,63 @@
 
         /** Disabled until Android provides sufficient guidelines for the usage of Vendor SPL. */
         internal var USE_VENDOR_SPL = false
+
+        /**
+         * Retrieves the specific security patch level for a given component based on a security
+         * patch level string. This method determines the type of [SecurityPatchLevel] to construct
+         * based on the component type, interpreting the string as a date for date-based components
+         * or as a version number for versioned components.
+         *
+         * @param component The component indicating which type of component's patch level is being
+         *   requested.
+         * @param securityPatchLevel The string representation of the security patch level, which
+         *   could be a date or a version number.
+         * @return A [SecurityPatchLevel] instance corresponding to the specified component and
+         *   patch level string.
+         * @throws IllegalArgumentException If the input string is not in a valid format for the
+         *   specified component type, or if the component requires a specific format that the
+         *   string does not meet.
+         */
+        @JvmStatic
+        public fun getComponentSecurityPatchLevel(
+            @Component component: String,
+            securityPatchLevel: String
+        ): SecurityPatchLevel {
+            val exception = IllegalArgumentException("Unknown component: $component")
+            return when (component) {
+                COMPONENT_SYSTEM,
+                COMPONENT_SYSTEM_MODULES,
+                COMPONENT_VENDOR -> {
+                    if (component == COMPONENT_VENDOR && !USE_VENDOR_SPL) {
+                        throw exception
+                    }
+                    // These components are expected to use DateBasedSpl
+                    DateBasedSecurityPatchLevel.fromString(securityPatchLevel)
+                }
+                COMPONENT_KERNEL -> {
+                    // These components are expected to use VersionedSpl
+                    VersionedSecurityPatchLevel.fromString(securityPatchLevel)
+                }
+                else -> throw exception
+            }
+        }
+
+        /**
+         * Constructs a URL for fetching vulnerability reports based on the device's Android
+         * version.
+         *
+         * @param serverUrl The base URL of the server where vulnerability reports are stored.
+         * @return A fully constructed URL pointing to the specific vulnerability report for this
+         *   device.
+         */
+        @JvmStatic
+        @RequiresApi(26)
+        public fun getVulnerabilityReportUrl(
+            serverUrl: Uri = Uri.parse(DEFAULT_VULNERABILITY_REPORTS_URL)
+        ): Uri {
+            val newEndpoint = "v1/android_sdk_${Build.VERSION.SDK_INT}.json"
+            return serverUrl.buildUpon().appendEncodedPath(newEndpoint).build()
+        }
     }
 
     /** Annotation for defining the component to use. */
@@ -162,6 +242,13 @@
         public companion object {
             private val DATE_FORMATS = listOf("yyyy-MM", "yyyy-MM-dd")
 
+            /**
+             * Creates a new [DateBasedSecurityPatchLevel] from a string representation of the date.
+             *
+             * @param value The date string in the format of [DATE_FORMATS].
+             * @return A new [DateBasedSecurityPatchLevel] representing the date.
+             * @throws IllegalArgumentException if the date string is not in the correct format.
+             */
             @JvmStatic
             public fun fromString(value: String): DateBasedSecurityPatchLevel {
                 var date: Date? = null
@@ -227,6 +314,14 @@
     ) : SecurityPatchLevel() {
 
         public companion object {
+            /**
+             * Creates a new [VersionedSecurityPatchLevel] from a string representation of the
+             * version.
+             *
+             * @param value The version string in the format of "major.minor.build.patch".
+             * @return A new [VersionedSecurityPatchLevel] representing the version.
+             * @throws IllegalArgumentException if the version string is not in the correct format.
+             */
             @JvmStatic
             public fun fromString(value: String): VersionedSecurityPatchLevel {
                 val parts = value.split(".")
@@ -329,8 +424,7 @@
      * @return A list of strings representing system module identifiers.
      */
     internal fun getSystemModules(): List<String> {
-        // Use the provided systemModules if not empty; otherwise, use defaultSystemModules
-        return systemModules.ifEmpty { DEFAULT_SYSTEM_MODULES }
+        return systemModulePackageNames.ifEmpty { DEFAULT_SYSTEM_MODULES }
     }
 
     /**
@@ -338,16 +432,10 @@
      * of the input JSON and constructs a [VulnerabilityReport] object, preparing the class to
      * provide published and available security state information.
      *
-     * The recommended pattern of usage:
-     * - create SecurityPatchState object
-     * - call getVulnerabilityReportUrl()
-     * - download JSON file containing vulnerability report data
-     * - call loadVulnerabilityReport()
-     * - call getPublishedSecurityPatchLevel() or other APIs
-     *
      * @param jsonString The JSON string containing the vulnerability data.
      * @throws IllegalArgumentException if the JSON input is malformed or contains invalid data.
      */
+    @WorkerThread
     public fun loadVulnerabilityReport(jsonString: String) {
         val result: VulnerabilityReport
 
@@ -426,27 +514,6 @@
         vulnerabilityReport = result
     }
 
-    /**
-     * Constructs a URL for fetching vulnerability reports based on the device's Android version.
-     *
-     * @param serverUrl The base URL of the server where vulnerability reports are stored.
-     * @return A fully constructed URL pointing to the specific vulnerability report for this
-     *   device.
-     * @throws IllegalArgumentException if the Android SDK version is unsupported.
-     */
-    @RequiresApi(26)
-    public fun getVulnerabilityReportUrl(serverUrl: Uri): Uri {
-        val androidSdk = securityStateManager.getAndroidSdkInt()
-        if (androidSdk < 26) {
-            throw IllegalArgumentException(
-                "Unsupported SDK version (must be > 25), found $androidSdk."
-            )
-        }
-
-        val newEndpoint = "v1/android_sdk_$androidSdk.json"
-        return serverUrl.buildUpon().appendEncodedPath(newEndpoint).build()
-    }
-
     private fun getMaxComponentSecurityPatchLevel(
         @Component component: String
     ): DateBasedSecurityPatchLevel? {
@@ -488,7 +555,7 @@
             try {
                 packageSpl =
                     DateBasedSecurityPatchLevel.fromString(
-                        securityStateManager.getPackageVersion(module)
+                        securityStateManagerCompat.getPackageVersion(module)
                     )
             } catch (e: Exception) {
                 // Prevent malformed package versions from interrupting the loop.
@@ -541,7 +608,8 @@
      * @throws IllegalArgumentException if the component name is unrecognized.
      */
     public open fun getDeviceSecurityPatchLevel(@Component component: String): SecurityPatchLevel {
-        val globalSecurityState = securityStateManager.getGlobalSecurityState(getSystemModules()[0])
+        val globalSecurityState =
+            securityStateManagerCompat.getGlobalSecurityState(getSystemModules()[0])
 
         return when (component) {
             COMPONENT_SYSTEM_MODULES -> {
@@ -700,45 +768,6 @@
     }
 
     /**
-     * Retrieves the specific security patch level for a given component based on a security patch
-     * level string. This method determines the type of [SecurityPatchLevel] to construct based on
-     * the component type, interpreting the string as a date for date-based components or as a
-     * version number for versioned components.
-     *
-     * @param component The component indicating which type of component's patch level is being
-     *   requested.
-     * @param securityPatchLevel The string representation of the security patch level, which could
-     *   be a date or a version number.
-     * @return A [SecurityPatchLevel] instance corresponding to the specified component and patch
-     *   level string.
-     * @throws IllegalArgumentException If the input string is not in a valid format for the
-     *   specified component type, or if the component requires a specific format that the string
-     *   does not meet.
-     */
-    public open fun getComponentSecurityPatchLevel(
-        @Component component: String,
-        securityPatchLevel: String
-    ): SecurityPatchLevel {
-        val exception = IllegalArgumentException("Unknown component: $component")
-        return when (component) {
-            COMPONENT_SYSTEM,
-            COMPONENT_SYSTEM_MODULES,
-            COMPONENT_VENDOR -> {
-                if (component == COMPONENT_VENDOR && !USE_VENDOR_SPL) {
-                    throw exception
-                }
-                // These components are expected to use DateBasedSpl
-                DateBasedSecurityPatchLevel.fromString(securityPatchLevel)
-            }
-            COMPONENT_KERNEL -> {
-                // These components are expected to use VersionedSpl
-                VersionedSecurityPatchLevel.fromString(securityPatchLevel)
-            }
-            else -> throw exception
-        }
-    }
-
-    /**
      * Checks if all components of the device have their security patch levels up to date with the
      * published security patch levels. This method compares the device's current security patch
      * level against the latest published levels for each component.
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt b/security/security-state/src/main/java/androidx/security/state/SecurityStateManagerCompat.kt
similarity index 88%
rename from security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt
rename to security/security-state/src/main/java/androidx/security/state/SecurityStateManagerCompat.kt
index 2676d6c..d5abdca 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityStateManagerCompat.kt
@@ -40,7 +40,7 @@
  * security-related information, which is crucial for maintaining the security integrity of the
  * device.
  */
-public open class SecurityStateManager(private val context: Context) {
+public open class SecurityStateManagerCompat(private val context: Context) {
 
     public companion object {
         private const val TAG = "SecurityStateManager"
@@ -76,26 +76,24 @@
      * and module information into a Bundle. This method can optionally use Google's module metadata
      * providers to enhance the data returned.
      *
-     * @param moduleMetadataProvider Specifies package name for system modules metadata.
+     * @param moduleMetadataProviderPackageName Specifies package name for system modules metadata.
      * @return A Bundle containing keys and values representing the security state of the system,
      *   vendor, and kernel.
      */
-    @SuppressLint("NewApi") // Lint does not detect version check below.
-    public open fun getGlobalSecurityState(moduleMetadataProvider: String? = null): Bundle {
-        if (getAndroidSdkInt() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+    public open fun getGlobalSecurityState(
+        moduleMetadataProviderPackageName: String = ANDROID_MODULE_METADATA_PROVIDER
+    ): Bundle {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
             return getGlobalSecurityStateFromService()
         }
         return Bundle().apply {
-            if (getAndroidSdkInt() >= Build.VERSION_CODES.M) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 putString(KEY_SYSTEM_SPL, Build.VERSION.SECURITY_PATCH)
                 if (USE_VENDOR_SPL) {
                     putString(KEY_VENDOR_SPL, getVendorSpl())
                 }
             }
-            if (getAndroidSdkInt() >= Build.VERSION_CODES.Q) {
-                val moduleMetadataProviderPackageName =
-                    moduleMetadataProvider ?: ANDROID_MODULE_METADATA_PROVIDER
-
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                 if (moduleMetadataProviderPackageName.isNotEmpty()) {
                     putString(
                         moduleMetadataProviderPackageName,
@@ -107,7 +105,9 @@
             if (kernelVersion.isNotEmpty()) {
                 putString(KEY_KERNEL_VERSION, kernelVersion)
             }
-            addWebViewPackages(this)
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                addWebViewPackages(this)
+            }
         }
     }
 
@@ -159,9 +159,6 @@
      */
     @RequiresApi(26)
     private fun addWebViewPackages(bundle: Bundle) {
-        if (getAndroidSdkInt() < Build.VERSION_CODES.O) {
-            return
-        }
         val packageName = getCurrentWebViewPackageName()
         if (packageName.isNotEmpty()) {
             bundle.putString(packageName, getPackageVersion(packageName))
@@ -187,15 +184,6 @@
     }
 
     /**
-     * Retrieves the SDK version of the current Android system.
-     *
-     * @return the SDK version as an integer.
-     */
-    internal open fun getAndroidSdkInt(): Int {
-        return Build.VERSION.SDK_INT
-    }
-
-    /**
      * Safely retrieves the current security patch level of the device's operating system. This
      * method ensures compatibility by checking the Android version before attempting to access APIs
      * that are not available on older versions.
@@ -203,9 +191,8 @@
      * @return A string representing the current security patch level, or empty string if it cannot
      *   be retrieved.
      */
-    @SuppressLint("NewApi") // Lint does not detect version check below.
     internal fun getSecurityPatchLevelSafe(): String {
-        return if (getAndroidSdkInt() >= Build.VERSION_CODES.M) {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             Build.VERSION.SECURITY_PATCH
         } else {
             ""
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
index 440fd0f6..a09f5b7 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
@@ -21,6 +21,7 @@
 import android.os.Build
 import android.os.Bundle
 import androidx.annotation.RequiresApi
+import androidx.security.state.SecurityPatchState.Companion.getComponentSecurityPatchLevel
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -39,12 +40,13 @@
 class SecurityPatchStateTest {
 
     private val mockContext: Context = mock<Context>()
-    private val mockSecurityStateManager: SecurityStateManager = mock<SecurityStateManager> {}
+    private val mockSecurityStateManagerCompat: SecurityStateManagerCompat =
+        mock<SecurityStateManagerCompat> {}
     private lateinit var securityState: SecurityPatchState
 
     @Before
     fun setup() {
-        securityState = SecurityPatchState(mockContext, listOf(), mockSecurityStateManager)
+        securityState = SecurityPatchState(mockContext, listOf(), mockSecurityStateManagerCompat)
     }
 
     @Test
@@ -54,11 +56,7 @@
 
     @Test
     fun testGetComponentSecurityPatchLevel_withSystemComponent_returnsDateBasedSpl() {
-        val spl =
-            securityState.getComponentSecurityPatchLevel(
-                SecurityPatchState.COMPONENT_SYSTEM,
-                "2022-01-01"
-            )
+        val spl = getComponentSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM, "2022-01-01")
         assertTrue(spl is SecurityPatchState.DateBasedSecurityPatchLevel)
         assertEquals("2022-01-01", spl.toString())
     }
@@ -66,11 +64,7 @@
     @Test
     fun testGetComponentSecurityPatchLevel_withVendorComponent_whenVendorIsEnabled_returnsDateBasedSpl() {
         SecurityPatchState.Companion.USE_VENDOR_SPL = true
-        val spl =
-            securityState.getComponentSecurityPatchLevel(
-                SecurityPatchState.COMPONENT_VENDOR,
-                "2022-01-01"
-            )
+        val spl = getComponentSecurityPatchLevel(SecurityPatchState.COMPONENT_VENDOR, "2022-01-01")
         assertTrue(spl is SecurityPatchState.DateBasedSecurityPatchLevel)
         assertEquals("2022-01-01", spl.toString())
     }
@@ -79,48 +73,28 @@
     fun testGetComponentSecurityPatchLevel_withVendorComponent_whenVendorIsDisabled_throwsException() {
         SecurityPatchState.Companion.USE_VENDOR_SPL = false
 
-        securityState.getComponentSecurityPatchLevel(
-            SecurityPatchState.COMPONENT_VENDOR,
-            "2022-01-01"
-        )
+        getComponentSecurityPatchLevel(SecurityPatchState.COMPONENT_VENDOR, "2022-01-01")
     }
 
     @Test
     fun testGetComponentSecurityPatchLevel_withKernelComponent_returnsVersionedSpl() {
-        val spl =
-            securityState.getComponentSecurityPatchLevel(
-                SecurityPatchState.COMPONENT_KERNEL,
-                "1.2.3.4"
-            )
+        val spl = getComponentSecurityPatchLevel(SecurityPatchState.COMPONENT_KERNEL, "1.2.3.4")
         assertTrue(spl is SecurityPatchState.VersionedSecurityPatchLevel)
         assertEquals("1.2.3.4", spl.toString())
     }
 
     @Test(expected = IllegalArgumentException::class)
     fun testGetComponentSecurityPatchLevel_withInvalidDateBasedInput_throwsException() {
-        securityState.getComponentSecurityPatchLevel(
-            SecurityPatchState.COMPONENT_SYSTEM,
-            "invalid-date"
-        )
+        getComponentSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM, "invalid-date")
     }
 
     @Test(expected = IllegalArgumentException::class)
     fun testGetComponentSecurityPatchLevel_withInvalidVersionedInput_throwsException() {
-        securityState.getComponentSecurityPatchLevel(
-            SecurityPatchState.COMPONENT_KERNEL,
-            "invalid-version"
-        )
-    }
-
-    @RequiresApi(Build.VERSION_CODES.O)
-    @Config(maxSdk = Build.VERSION_CODES.N_MR1)
-    @Test(expected = IllegalArgumentException::class)
-    fun testGetVulnerabilityReportUrl_withUnsupportedSdk_throwsException() {
-        securityState.getVulnerabilityReportUrl(Uri.parse("https://example.com"))
+        getComponentSecurityPatchLevel(SecurityPatchState.COMPONENT_KERNEL, "invalid-version")
     }
 
     @Test
-    fun testParseVulnerabilityReport_validJson_returnsCorrectData() {
+    fun testLoadVulnerabilityReport_validJson_returnsCorrectData() {
         val jsonString =
             """
             {
@@ -159,7 +133,7 @@
     }
 
     @Test(expected = IllegalArgumentException::class)
-    fun testParseVulnerabilityReport_invalidAsb_throwsIllegalArgumentException() {
+    fun testLoadVulnerabilityReport_invalidAsb_throwsIllegalArgumentException() {
         val jsonString =
             """
             {
@@ -181,21 +155,20 @@
     }
 
     @Test(expected = IllegalArgumentException::class)
-    fun testParseVulnerabilityReport_invalidJson_throwsIllegalArgumentException() {
+    fun testLoadVulnerabilityReport_invalidJson_throwsIllegalArgumentException() {
         val invalidJson = "{ invalid json }"
         securityState.loadVulnerabilityReport(invalidJson)
     }
 
     @RequiresApi(Build.VERSION_CODES.O)
+    @Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
     @Test
     fun testGetVulnerabilityReportUrl_validSdkVersion_returnsCorrectUrl() {
         val sdkVersion = 34 // Android 14
         val baseUrl = SecurityPatchState.DEFAULT_VULNERABILITY_REPORTS_URL
-        val expectedUrl = "$baseUrl/v1/android_sdk_34.json"
+        val expectedUrl = "$baseUrl/v1/android_sdk_$sdkVersion.json"
 
-        doReturn(sdkVersion).`when`(mockSecurityStateManager).getAndroidSdkInt()
-
-        val actualUrl = securityState.getVulnerabilityReportUrl(Uri.parse(baseUrl)).toString()
+        val actualUrl = SecurityPatchState.getVulnerabilityReportUrl(Uri.parse(baseUrl)).toString()
         assertEquals(expectedUrl, actualUrl)
     }
 
@@ -205,7 +178,8 @@
         val bundle = Bundle()
         bundle.putString("system_spl", systemSpl)
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
 
         val spl =
             securityState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
@@ -219,8 +193,8 @@
     fun testGetDeviceSpl_noSplAvailable_throwsIllegalStateException() {
         val bundle = Bundle()
         // SPL not set in the bundle for the system component
-        doReturn("").`when`(mockSecurityStateManager).getPackageVersion(Mockito.anyString())
-        doReturn(bundle).`when`(mockSecurityStateManager).getGlobalSecurityState(anyString())
+        doReturn("").`when`(mockSecurityStateManagerCompat).getPackageVersion(Mockito.anyString())
+        doReturn(bundle).`when`(mockSecurityStateManagerCompat).getGlobalSecurityState(anyString())
 
         securityState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
     }
@@ -231,6 +205,18 @@
     }
 
     @Test
+    fun testGetPublishedSpl_doesNotThrowWhenVulnerabilityReportLoadedFromConstructor() {
+        securityState =
+            SecurityPatchState(
+                mockContext,
+                listOf(),
+                mockSecurityStateManagerCompat,
+                vulnerabilityReportJsonString = generateMockReport("system", "2023-01-01")
+            )
+        securityState.getPublishedSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
+    }
+
+    @Test
     fun testGetDeviceSpl_ReturnsCorrectSplForUnpatchedSystemModules() {
         val jsonInput =
             """
@@ -262,15 +248,19 @@
 
         securityState.loadVulnerabilityReport(jsonInput)
 
-        `when`(mockSecurityStateManager.getPackageVersion("com.google.android.modulemetadata"))
+        `when`(
+                mockSecurityStateManagerCompat.getPackageVersion(
+                    "com.google.android.modulemetadata"
+                )
+            )
             .thenReturn("2022-01-01")
-        `when`(mockSecurityStateManager.getPackageVersion("com.google.mainline.telemetry"))
+        `when`(mockSecurityStateManagerCompat.getPackageVersion("com.google.mainline.telemetry"))
             .thenReturn("2023-05-01")
-        `when`(mockSecurityStateManager.getPackageVersion("com.google.mainline.adservices"))
+        `when`(mockSecurityStateManagerCompat.getPackageVersion("com.google.mainline.adservices"))
             .thenReturn("2022-05-01")
-        `when`(mockSecurityStateManager.getPackageVersion("com.google.mainline.go.primary"))
+        `when`(mockSecurityStateManagerCompat.getPackageVersion("com.google.mainline.go.primary"))
             .thenReturn("2021-05-01")
-        `when`(mockSecurityStateManager.getPackageVersion("com.google.mainline.go.telemetry"))
+        `when`(mockSecurityStateManagerCompat.getPackageVersion("com.google.mainline.go.telemetry"))
             .thenReturn("2024-05-01")
 
         val spl =
@@ -779,8 +769,9 @@
         bundle.putString("kernel_version", "5.4.123")
         bundle.putString("com.google.android.modulemetadata", "2023-10-05")
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn("2023-10-05").`when`(mockSecurityStateManager).getPackageVersion(anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn("2023-10-05").`when`(mockSecurityStateManagerCompat).getPackageVersion(anyString())
 
         val jsonInput =
             """
@@ -824,8 +815,9 @@
         bundle.putString("kernel_version", "5.4.123")
         bundle.putString("com.google.android.modulemetadata", "2023-10-05")
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn("2023-10-05").`when`(mockSecurityStateManager).getPackageVersion(anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn("2023-10-05").`when`(mockSecurityStateManagerCompat).getPackageVersion(anyString())
 
         val jsonInput =
             """
@@ -869,8 +861,9 @@
         bundle.putString("kernel_version", "5.4.123")
         bundle.putString("com.google.android.modulemetadata", "2023-10-05")
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn("2023-10-05").`when`(mockSecurityStateManager).getPackageVersion(anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn("2023-10-05").`when`(mockSecurityStateManagerCompat).getPackageVersion(anyString())
 
         val jsonInput =
             """
@@ -911,8 +904,9 @@
         bundle.putString("system_spl", "2022-01-01")
         bundle.putString("com.google.android.modulemetadata", "2023-10-05")
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn("2023-10-05").`when`(mockSecurityStateManager).getPackageVersion(anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn("2023-10-05").`when`(mockSecurityStateManagerCompat).getPackageVersion(anyString())
 
         val jsonInput =
             """
@@ -984,8 +978,11 @@
         bundle.putString("vendor_spl", systemSpl)
         bundle.putString("com.google.android.modulemetadata", systemSpl)
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn(systemSpl).`when`(mockSecurityStateManager).getPackageVersion(Mockito.anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn(systemSpl)
+            .`when`(mockSecurityStateManagerCompat)
+            .getPackageVersion(Mockito.anyString())
 
         assertTrue(securityState.areCvesPatched(listOf("CVE-2023-0001", "CVE-2023-0002")))
     }
@@ -1027,8 +1024,11 @@
         bundle.putString("vendor_spl", systemSpl)
         bundle.putString("com.google.android.modulemetadata", systemSpl)
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn(systemSpl).`when`(mockSecurityStateManager).getPackageVersion(Mockito.anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn(systemSpl)
+            .`when`(mockSecurityStateManagerCompat)
+            .getPackageVersion(Mockito.anyString())
 
         assertFalse(
             securityState.areCvesPatched(listOf("CVE-2023-0010", "CVE-2023-0001", "CVE-2023-0002"))
@@ -1072,8 +1072,11 @@
         bundle.putString("vendor_spl", systemSpl)
         bundle.putString("com.google.android.modulemetadata", systemSpl)
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn(systemSpl).`when`(mockSecurityStateManager).getPackageVersion(Mockito.anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn(systemSpl)
+            .`when`(mockSecurityStateManagerCompat)
+            .getPackageVersion(Mockito.anyString())
 
         assertFalse(
             securityState.areCvesPatched(listOf("CVE-2024-1010", "CVE-2023-0001", "CVE-2023-0002"))
@@ -1118,8 +1121,11 @@
         bundle.putString("vendor_spl", systemSpl)
         bundle.putString("com.google.android.modulemetadata", systemSpl)
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn(systemSpl).`when`(mockSecurityStateManager).getPackageVersion(Mockito.anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn(systemSpl)
+            .`when`(mockSecurityStateManagerCompat)
+            .getPackageVersion(Mockito.anyString())
 
         assertTrue(securityState.areCvesPatched(listOf("CVE-2023-0010")))
     }
@@ -1162,8 +1168,11 @@
         bundle.putString("vendor_spl", systemSpl)
         bundle.putString("com.google.android.modulemetadata", systemSpl)
 
-        `when`(mockSecurityStateManager.getGlobalSecurityState(anyString())).thenReturn(bundle)
-        doReturn(systemSpl).`when`(mockSecurityStateManager).getPackageVersion(Mockito.anyString())
+        `when`(mockSecurityStateManagerCompat.getGlobalSecurityState(anyString()))
+            .thenReturn(bundle)
+        doReturn(systemSpl)
+            .`when`(mockSecurityStateManagerCompat)
+            .getPackageVersion(Mockito.anyString())
 
         assertFalse(securityState.areCvesPatched(listOf("CVE-2023-0010")))
     }
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityStateManagerCompatTest.kt
similarity index 81%
rename from security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt
rename to security/security-state/src/test/java/androidx/security/state/SecurityStateManagerCompatTest.kt
index 19c2f1d..961007a 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityStateManagerCompatTest.kt
@@ -34,15 +34,15 @@
 import org.robolectric.annotation.Config
 
 @RunWith(JUnit4::class)
-class SecurityStateManagerTest {
+class SecurityStateManagerCompatTest {
 
     private val packageManager: PackageManager = mock<PackageManager>()
     private val context: Context = mock<Context>() { on { packageManager } doReturn packageManager }
-    private lateinit var securityStateManager: SecurityStateManager
+    private lateinit var securityStateManagerCompat: SecurityStateManagerCompat
 
     @Before
     fun setUp() {
-        securityStateManager = SecurityStateManager(context)
+        securityStateManagerCompat = SecurityStateManagerCompat(context)
     }
 
     @Config(minSdk = Build.VERSION_CODES.Q)
@@ -56,7 +56,7 @@
             .thenReturn(PackageInfo().apply { versionName = "" })
 
         val result =
-            securityStateManager.getGlobalSecurityState("com.google.android.modulemetadata")
+            securityStateManagerCompat.getGlobalSecurityState("com.google.android.modulemetadata")
         assertEquals(
             expectedBundle.getString("com.google.android.modulemetadata"),
             result.getString("com.google.android.modulemetadata")
@@ -68,7 +68,7 @@
         Mockito.`when`(packageManager.getPackageInfo(Mockito.anyString(), Mockito.eq(0)))
             .thenThrow(PackageManager.NameNotFoundException())
 
-        val result = securityStateManager.getPackageVersion("non.existent.package")
+        val result = securityStateManagerCompat.getPackageVersion("non.existent.package")
         assertTrue(result.isEmpty())
     }
 
@@ -77,25 +77,25 @@
         // This method would normally require reading from the file system,
         // but we can mock this by pretending the expected output of the file read is known.
         val originalKernelVersionMethod =
-            securityStateManager::class.java.getDeclaredMethod("getKernelVersion")
+            securityStateManagerCompat::class.java.getDeclaredMethod("getKernelVersion")
         originalKernelVersionMethod.isAccessible = true
-        val kernelVersion = originalKernelVersionMethod.invoke(securityStateManager) as String
+        val kernelVersion = originalKernelVersionMethod.invoke(securityStateManagerCompat) as String
         assertNotNull(kernelVersion)
     }
 
     @Test
     fun testGetVendorSpl() {
         val originalVendorSplMethod =
-            securityStateManager::class.java.getDeclaredMethod("getVendorSpl")
+            securityStateManagerCompat::class.java.getDeclaredMethod("getVendorSpl")
         originalVendorSplMethod.isAccessible = true
-        val vendorSpl = originalVendorSplMethod.invoke(securityStateManager) as String
+        val vendorSpl = originalVendorSplMethod.invoke(securityStateManagerCompat) as String
         assertNotNull(vendorSpl)
     }
 
     @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
     @Test
     fun testGetSecurityPatchLevelSafe_API_Level_Below_M() {
-        val result = securityStateManager.getSecurityPatchLevelSafe()
+        val result = securityStateManagerCompat.getSecurityPatchLevelSafe()
         assertEquals("", result)
     }
 }
diff --git a/settings.gradle b/settings.gradle
index b12b850..cf922fc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1159,6 +1159,8 @@
 includeProject(":work:work-rxjava3", [BuildType.MAIN])
 includeProject(":work:work-testing", [BuildType.MAIN])
 includeProject(":xr:arcore:arcore", [BuildType.XR])
+includeProject(":xr:arcore:integration-tests:whitebox", [BuildType.XR])
+includeProject(":xr:assets", [BuildType.XR])
 includeProject(":xr:compose:compose", [BuildType.XR])
 includeProject(":xr:compose:compose-testing", [BuildType.XR])
 includeProject(":xr:compose:material3:material3", [BuildType.XR])
diff --git a/test/uiautomator/uiautomator/api/current.txt b/test/uiautomator/uiautomator/api/current.txt
index 718c737..6b48582 100644
--- a/test/uiautomator/uiautomator/api/current.txt
+++ b/test/uiautomator/uiautomator/api/current.txt
@@ -99,6 +99,7 @@
 
   public final class Configurator {
     method public long getActionAcknowledgmentTimeout();
+    method public int getDefaultDisplayId();
     method public static androidx.test.uiautomator.Configurator getInstance();
     method @Deprecated public long getKeyInjectionDelay();
     method public long getScrollAcknowledgmentTimeout();
@@ -106,7 +107,9 @@
     method public int getUiAutomationFlags();
     method public long getWaitForIdleTimeout();
     method public long getWaitForSelectorTimeout();
+    method public androidx.test.uiautomator.Configurator resetDefaultDisplayId();
     method public androidx.test.uiautomator.Configurator setActionAcknowledgmentTimeout(long);
+    method public androidx.test.uiautomator.Configurator setDefaultDisplayId(int);
     method @Deprecated public androidx.test.uiautomator.Configurator setKeyInjectionDelay(long);
     method public androidx.test.uiautomator.Configurator setScrollAcknowledgmentTimeout(long);
     method public androidx.test.uiautomator.Configurator setToolType(int);
diff --git a/test/uiautomator/uiautomator/api/restricted_current.txt b/test/uiautomator/uiautomator/api/restricted_current.txt
index 718c737..6b48582 100644
--- a/test/uiautomator/uiautomator/api/restricted_current.txt
+++ b/test/uiautomator/uiautomator/api/restricted_current.txt
@@ -99,6 +99,7 @@
 
   public final class Configurator {
     method public long getActionAcknowledgmentTimeout();
+    method public int getDefaultDisplayId();
     method public static androidx.test.uiautomator.Configurator getInstance();
     method @Deprecated public long getKeyInjectionDelay();
     method public long getScrollAcknowledgmentTimeout();
@@ -106,7 +107,9 @@
     method public int getUiAutomationFlags();
     method public long getWaitForIdleTimeout();
     method public long getWaitForSelectorTimeout();
+    method public androidx.test.uiautomator.Configurator resetDefaultDisplayId();
     method public androidx.test.uiautomator.Configurator setActionAcknowledgmentTimeout(long);
+    method public androidx.test.uiautomator.Configurator setDefaultDisplayId(int);
     method @Deprecated public androidx.test.uiautomator.Configurator setKeyInjectionDelay(long);
     method public androidx.test.uiautomator.Configurator setScrollAcknowledgmentTimeout(long);
     method public androidx.test.uiautomator.Configurator setToolType(int);
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
index 04055d4..9fa089f 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
@@ -16,6 +16,8 @@
 
 package androidx.test.uiautomator;
 
+import static android.view.Display.INVALID_DISPLAY;
+
 import static java.util.Objects.requireNonNull;
 
 import androidx.annotation.IntRange;
@@ -69,7 +71,10 @@
 
 
     /** Clients should not instanciate this class directly. Use the {@link By} factory class instead. */
-    BySelector() { }
+    BySelector() {
+        final int defaultDisplayId = Configurator.getInstance().getDefaultDisplayId();
+        mDisplayId = defaultDisplayId == INVALID_DISPLAY ? null : defaultDisplayId;
+    }
 
     /**
      * Constructs a new {@link BySelector} and copies the criteria from {@code original}.
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Configurator.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Configurator.java
index c8fd7e9..a2e2aec 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Configurator.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Configurator.java
@@ -16,6 +16,8 @@
 
 package androidx.test.uiautomator;
 
+import static android.view.Display.INVALID_DISPLAY;
+
 import android.view.MotionEvent;
 
 import org.jspecify.annotations.NonNull;
@@ -48,6 +50,9 @@
     static final int DEFAULT_UIAUTOMATION_FLAGS = 0;
     private int mUiAutomationFlags = DEFAULT_UIAUTOMATION_FLAGS;
 
+    // Default display ID when obtaining a BySelector instance
+    private int mDefaultDisplayId = INVALID_DISPLAY;
+
     // Singleton instance.
     private static Configurator sConfigurator;
 
@@ -242,4 +247,39 @@
     public int getUiAutomationFlags() {
         return mUiAutomationFlags;
     }
+
+    /**
+     * Sets the default display ID to use when obtaining a
+     * {@link androidx.test.uiautomator.BySelector} instance. To avoid interfering with other tests,
+     * the caller must call {@link #resetDefaultDisplayId} when the test is finished.
+     *
+     * @param defaultDisplayId the default display ID to use
+     * @return self
+     */
+    public @NonNull Configurator setDefaultDisplayId(int defaultDisplayId) {
+        mDefaultDisplayId = defaultDisplayId;
+        return this;
+    }
+
+    /**
+     * Resets the default display ID to use when obtaining a
+     * {@link androidx.test.uiautomator.BySelector} instance.
+     *
+     * @return self
+     */
+    public @NonNull Configurator resetDefaultDisplayId() {
+        mDefaultDisplayId = INVALID_DISPLAY;
+        return this;
+    }
+
+    /**
+     * Gets the default display ID to use when obtaining a
+     * {@link androidx.test.uiautomator.BySelector} instance, or returns
+     * {@link android.view.Display#INVALID_DISPLAY} if the default display ID is not set yet.
+     *
+     * @return the default display ID
+     */
+    public int getDefaultDisplayId() {
+        return mDefaultDisplayId;
+    }
 }
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index df002aa..ca1d072 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -156,8 +156,10 @@
   public final class ButtonGroupDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues fullWidthPaddings();
     method public float getExpansionWidth();
+    method public float getMinWidth();
     method public float getSpacing();
     property public final float ExpansionWidth;
+    property public final float MinWidth;
     property public final float Spacing;
     field public static final androidx.wear.compose.material3.ButtonGroupDefaults INSTANCE;
   }
@@ -167,7 +169,7 @@
   }
 
   public interface ButtonGroupScope {
-    method public androidx.compose.ui.Modifier enlargeOnPress(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method public androidx.compose.ui.Modifier animateWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.InteractionSource interactionSource);
     method public androidx.compose.ui.Modifier minWidth(androidx.compose.ui.Modifier, optional float minWidth);
     method public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, fromInclusive=false) float weight);
   }
@@ -1648,12 +1650,10 @@
   }
 
   public final class TimeTextDefaults {
-    method public float getAutoTextWeight();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimeSource rememberTimeSource(String timeFormat);
     method @androidx.compose.runtime.Composable public String timeFormat();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
-    property public final float AutoTextWeight;
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.CurvedTextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
     property public static final float MaxSweepAngle;
     property public static final String TimeFormat12Hours;
@@ -1665,14 +1665,9 @@
   }
 
   public final class TimeTextKt {
-    method @androidx.compose.runtime.Composable public static void TimeText(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedModifier curvedModifier, optional float maxSweepAngle, optional androidx.wear.compose.material3.TimeSource timeSource, optional androidx.compose.ui.text.TextStyle timeTextStyle, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.material3.TimeTextScope,kotlin.Unit> content);
-  }
-
-  public abstract sealed class TimeTextScope {
-    method public abstract void composable(kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method public abstract void separator(optional androidx.compose.ui.text.TextStyle? style);
-    method public abstract void text(String text, optional androidx.compose.ui.text.TextStyle? style, optional float weight);
-    method public abstract void time();
+    method @androidx.compose.runtime.Composable public static void TimeText(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedModifier curvedModifier, optional float maxSweepAngle, optional androidx.wear.compose.material3.TimeSource timeSource, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.CurvedScope,? super java.lang.String,kotlin.Unit> content);
+    method public static void timeTextCurvedText(androidx.wear.compose.foundation.CurvedScope, String time, optional androidx.wear.compose.foundation.CurvedTextStyle? style);
+    method public static void timeTextSeparator(androidx.wear.compose.foundation.CurvedScope, optional androidx.wear.compose.foundation.CurvedTextStyle? curvedTextStyle, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding);
   }
 
   public final class TouchTargetAwareSizeKt {
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index df002aa..ca1d072 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -156,8 +156,10 @@
   public final class ButtonGroupDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues fullWidthPaddings();
     method public float getExpansionWidth();
+    method public float getMinWidth();
     method public float getSpacing();
     property public final float ExpansionWidth;
+    property public final float MinWidth;
     property public final float Spacing;
     field public static final androidx.wear.compose.material3.ButtonGroupDefaults INSTANCE;
   }
@@ -167,7 +169,7 @@
   }
 
   public interface ButtonGroupScope {
-    method public androidx.compose.ui.Modifier enlargeOnPress(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method public androidx.compose.ui.Modifier animateWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.InteractionSource interactionSource);
     method public androidx.compose.ui.Modifier minWidth(androidx.compose.ui.Modifier, optional float minWidth);
     method public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, fromInclusive=false) float weight);
   }
@@ -1648,12 +1650,10 @@
   }
 
   public final class TimeTextDefaults {
-    method public float getAutoTextWeight();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimeSource rememberTimeSource(String timeFormat);
     method @androidx.compose.runtime.Composable public String timeFormat();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
-    property public final float AutoTextWeight;
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.CurvedTextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
     property public static final float MaxSweepAngle;
     property public static final String TimeFormat12Hours;
@@ -1665,14 +1665,9 @@
   }
 
   public final class TimeTextKt {
-    method @androidx.compose.runtime.Composable public static void TimeText(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedModifier curvedModifier, optional float maxSweepAngle, optional androidx.wear.compose.material3.TimeSource timeSource, optional androidx.compose.ui.text.TextStyle timeTextStyle, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.material3.TimeTextScope,kotlin.Unit> content);
-  }
-
-  public abstract sealed class TimeTextScope {
-    method public abstract void composable(kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method public abstract void separator(optional androidx.compose.ui.text.TextStyle? style);
-    method public abstract void text(String text, optional androidx.compose.ui.text.TextStyle? style, optional float weight);
-    method public abstract void time();
+    method @androidx.compose.runtime.Composable public static void TimeText(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedModifier curvedModifier, optional float maxSweepAngle, optional androidx.wear.compose.material3.TimeSource timeSource, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.CurvedScope,? super java.lang.String,kotlin.Unit> content);
+    method public static void timeTextCurvedText(androidx.wear.compose.foundation.CurvedScope, String time, optional androidx.wear.compose.foundation.CurvedTextStyle? style);
+    method public static void timeTextSeparator(androidx.wear.compose.foundation.CurvedScope, optional androidx.wear.compose.foundation.CurvedTextStyle? curvedTextStyle, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding);
   }
 
   public final class TouchTargetAwareSizeKt {
diff --git a/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ScrollIndicatorBenchmark.kt b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ScrollIndicatorBenchmark.kt
new file mode 100644
index 0000000..de0e838
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ScrollIndicatorBenchmark.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.compose.material3.benchmark
+
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkDrawPerf
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ScrollIndicator
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ScrollIndicatorBenchmark {
+
+    @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+    private val testCaseFactory = { ScrollIndicatorTestCase() }
+
+    @Test
+    fun first_pixel() {
+        benchmarkRule.benchmarkToFirstPixel(testCaseFactory)
+    }
+
+    @Test
+    fun first_compose() {
+        benchmarkRule.benchmarkFirstCompose(testCaseFactory)
+    }
+
+    @Test
+    fun first_measure() {
+        benchmarkRule.benchmarkFirstMeasure(testCaseFactory)
+    }
+
+    @Test
+    fun first_layout() {
+        benchmarkRule.benchmarkFirstLayout(testCaseFactory)
+    }
+
+    @Test
+    fun first_draw() {
+        benchmarkRule.benchmarkFirstDraw(testCaseFactory)
+    }
+
+    @Test
+    fun layout() {
+        benchmarkRule.benchmarkLayoutPerf(testCaseFactory)
+    }
+
+    @Test
+    fun draw() {
+        benchmarkRule.benchmarkDrawPerf(testCaseFactory)
+    }
+}
+
+internal class ScrollIndicatorTestCase : LayeredComposeTestCase() {
+    @Composable
+    override fun MeasuredContent() {
+        ScrollIndicator(state = rememberScrollState())
+    }
+
+    @Composable
+    override fun ContentWrappers(content: @Composable () -> Unit) {
+        MaterialTheme { content() }
+    }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonGroupDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonGroupDemo.kt
index 76c8ba5..2832ea1 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonGroupDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonGroupDemo.kt
@@ -59,14 +59,14 @@
         ButtonGroup(Modifier.fillMaxWidth()) {
             Button(
                 onClick = {},
-                Modifier.enlargeOnPress(interactionSource1),
+                Modifier.animateWidth(interactionSource1),
                 interactionSource = interactionSource1
             ) {
                 Text("<", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
             }
             FilledIconButton(
                 onClick = {},
-                Modifier.enlargeOnPress(interactionSource2),
+                Modifier.animateWidth(interactionSource2),
                 interactionSource = interactionSource2
             ) {
                 Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -79,7 +79,7 @@
             }
             Button(
                 onClick = {},
-                Modifier.enlargeOnPress(interactionSource3),
+                Modifier.animateWidth(interactionSource3),
                 interactionSource = interactionSource3
             ) {
                 Text(">", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
@@ -145,7 +145,7 @@
             modifier
                 .height(IconToggleButtonDefaults.SmallButtonSize)
                 .fillMaxWidth()
-                .enlargeOnPress(interactionSource),
+                .animateWidth(interactionSource),
         onCheckedChange = { checked = !checked },
         shapes = shapes,
         interactionSource = interactionSource
@@ -168,7 +168,7 @@
             modifier
                 .height(TextToggleButtonDefaults.DefaultButtonSize)
                 .fillMaxWidth()
-                .enlargeOnPress(interactionSource),
+                .animateWidth(interactionSource),
         onCheckedChange = { checked = !checked },
         shapes = shapes,
         interactionSource = interactionSource
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ScrollAwayDemos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ScrollAwayDemos.kt
index ce9aa66..9b3d8c3 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ScrollAwayDemos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ScrollAwayDemos.kt
@@ -41,6 +41,7 @@
 import androidx.wear.compose.material3.ScreenStage
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.curvedText
 import androidx.wear.compose.material3.samples.ScrollAwaySample
 import androidx.wear.compose.material3.scrollAway
 
@@ -100,7 +101,7 @@
                         else ScreenStage.Idle
                     }
                 ),
-            content = { text("ScrollAway") }
+            content = { curvedText("ScrollAway") }
         )
     }
 }
@@ -151,7 +152,7 @@
                         else ScreenStage.Idle
                     }
                 ),
-            content = { text("ScrollAway") }
+            content = { curvedText("ScrollAway") }
         )
     }
 }
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
index 397f24d..d53c350 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
@@ -33,6 +33,7 @@
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.wear.compose.foundation.curvedComposable
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.material3.Button
@@ -43,13 +44,17 @@
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.TimeText
 import androidx.wear.compose.material3.TimeTextDefaults
+import androidx.wear.compose.material3.curvedText
 import androidx.wear.compose.material3.samples.TimeTextClockOnly
 import androidx.wear.compose.material3.samples.TimeTextWithStatus
+import androidx.wear.compose.material3.samples.TimeTextWithStatusEllipsized
+import androidx.wear.compose.material3.timeTextSeparator
 
 val TimeTextDemos =
     listOf(
         ComposableDemo("Clock only") { TimeTextClockOnly() },
         ComposableDemo("Clock with Status") { TimeTextWithStatus() },
+        ComposableDemo("Clock with Ellipsized Status") { TimeTextWithStatusEllipsized() },
         ComposableDemo("Clock with long Status") { TimeTextWithLongStatus() },
         ComposableDemo("Clock with Icon") { TimeTextWithIcon() },
         ComposableDemo("Clock with custom colors") { TimeTextWithCustomColors() },
@@ -61,10 +66,10 @@
 
 @Composable
 fun TimeTextWithLongStatus() {
-    TimeText {
-        text("Some long leading text")
-        separator()
-        time()
+    TimeText { time ->
+        curvedText("Some long leading text")
+        timeTextSeparator()
+        curvedText(time)
     }
 }
 
@@ -72,12 +77,12 @@
 fun TimeTextWithCustomColors() {
     val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
 
-    TimeText {
-        text("ETA", customStyle)
-        composable { Spacer(modifier = Modifier.size(4.dp)) }
-        text("12:48")
-        separator()
-        time()
+    TimeText { time ->
+        curvedText("ETA", style = customStyle)
+        curvedComposable { Spacer(modifier = Modifier.size(4.dp)) }
+        curvedText("12:48")
+        timeTextSeparator()
+        curvedText(time)
     }
 }
 
@@ -85,19 +90,19 @@
 fun TimeTextCustomSize() {
     val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Green, fontSize = 24.sp)
 
-    TimeText {
-        text("ETA", customStyle)
-        separator()
-        time()
+    TimeText { time ->
+        curvedText("ETA", style = customStyle)
+        timeTextSeparator()
+        curvedText(time)
     }
 }
 
 @Composable
 fun TimeTextWithIcon() {
-    TimeText {
-        time()
-        separator()
-        composable {
+    TimeText { time ->
+        curvedText(time)
+        timeTextSeparator()
+        curvedComposable {
             Icon(
                 imageVector = Icons.Filled.Favorite,
                 contentDescription = "Favorite",
@@ -151,13 +156,13 @@
             items(10) { Text("Some extra items ($it) to scroll", Modifier.padding(5.dp)) }
         }
         // Timetext later so it's on top.
-        TimeText { time() }
+        TimeText { time -> curvedText(time) }
     }
 }
 
 @Composable
 fun TimeTextOnScreenWhiteBackground() {
-    Box(Modifier.fillMaxSize().background(Color.White)) { TimeText { time() } }
+    Box(Modifier.fillMaxSize().background(Color.White)) { TimeText { time -> curvedText(time) } }
 }
 
 @Composable
@@ -166,7 +171,7 @@
         MaterialTheme(
             colorScheme = MaterialTheme.colorScheme.copy(background = Color.Transparent)
         ) {
-            TimeText { time() }
+            TimeText { time -> curvedText(time) }
         }
     }
 }
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TransformingLazyColumnDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TransformingLazyColumnDemo.kt
index 4b6646b..7fd48eb 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TransformingLazyColumnDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TransformingLazyColumnDemo.kt
@@ -159,7 +159,7 @@
                 ButtonGroup(Modifier.scrollTransform(this@item)) {
                     Button(
                         onClick = {},
-                        Modifier.enlargeOnPress(interactionSource1),
+                        Modifier.animateWidth(interactionSource1),
                         interactionSource = interactionSource1
                     ) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -168,7 +168,7 @@
                     }
                     Button(
                         onClick = {},
-                        Modifier.enlargeOnPress(interactionSource2),
+                        Modifier.animateWidth(interactionSource2),
                         interactionSource = interactionSource2
                     ) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonGroupSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonGroupSample.kt
index 9b6d145..19240bb 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonGroupSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonGroupSample.kt
@@ -19,7 +19,6 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
@@ -41,17 +40,17 @@
         ButtonGroup(Modifier.fillMaxWidth()) {
             Button(
                 onClick = {},
-                modifier = Modifier.enlargeOnPress(interactionSource1),
+                modifier = Modifier.animateWidth(interactionSource1),
                 interactionSource = interactionSource1
             ) {
-                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("L") }
+                Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("L") }
             }
             Button(
                 onClick = {},
-                modifier = Modifier.enlargeOnPress(interactionSource2),
+                modifier = Modifier.animateWidth(interactionSource2),
                 interactionSource = interactionSource2
             ) {
-                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("R") }
+                Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("R") }
             }
         }
     }
@@ -68,24 +67,24 @@
         ButtonGroup(Modifier.fillMaxWidth()) {
             Button(
                 onClick = {},
-                modifier = Modifier.enlargeOnPress(interactionSource1),
+                modifier = Modifier.animateWidth(interactionSource1),
                 interactionSource = interactionSource1
             ) {
-                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("A") }
+                Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("A") }
             }
             Button(
                 onClick = {},
-                modifier = Modifier.weight(1.5f).enlargeOnPress(interactionSource2),
+                modifier = Modifier.weight(1.5f).animateWidth(interactionSource2),
                 interactionSource = interactionSource2
             ) {
-                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("B") }
+                Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("B") }
             }
             Button(
                 onClick = {},
-                modifier = Modifier.enlargeOnPress(interactionSource3),
+                modifier = Modifier.animateWidth(interactionSource3),
                 interactionSource = interactionSource3
             ) {
-                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("C") }
+                Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("C") }
             }
         }
     }
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ScrollAwaySample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ScrollAwaySample.kt
index 225dd56f..42f1ba7 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ScrollAwaySample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ScrollAwaySample.kt
@@ -33,7 +33,9 @@
 import androidx.wear.compose.material3.ScreenStage
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.curvedText
 import androidx.wear.compose.material3.scrollAway
+import androidx.wear.compose.material3.timeTextSeparator
 
 @Sampled
 @Composable
@@ -73,10 +75,10 @@
                         if (state.isScrollInProgress) ScreenStage.Scrolling else ScreenStage.Idle
                     }
                 ),
-            content = {
-                text("ScrollAway")
-                separator()
-                time()
+            content = { time ->
+                curvedText("ScrollAway")
+                timeTextSeparator()
+                curvedText(time)
             }
         )
     }
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
index 020e098..47ba1e6 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
@@ -18,9 +18,14 @@
 
 import androidx.annotation.Sampled
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.weight
 import androidx.wear.compose.material3.MaterialTheme
 import androidx.wear.compose.material3.TimeText
 import androidx.wear.compose.material3.TimeTextDefaults
+import androidx.wear.compose.material3.curvedText
+import androidx.wear.compose.material3.timeTextSeparator
 
 @Sampled
 @Composable
@@ -34,9 +39,23 @@
 fun TimeTextWithStatus() {
     val primaryStyle =
         TimeTextDefaults.timeTextStyle(color = MaterialTheme.colorScheme.primaryContainer)
-    TimeText {
-        text("ETA 12:48", style = primaryStyle)
-        separator()
-        time()
+    TimeText { time ->
+        curvedText("ETA 12:48", style = primaryStyle)
+        timeTextSeparator()
+        curvedText(time)
+    }
+}
+
+@Sampled
+@Composable
+fun TimeTextWithStatusEllipsized() {
+    TimeText { time ->
+        curvedText(
+            "Long status that should be ellipsized.",
+            CurvedModifier.weight(1f),
+            overflow = TextOverflow.Ellipsis
+        )
+        timeTextSeparator()
+        curvedText(time)
     }
 }
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
index a6c2594..ba35ea0 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
@@ -18,16 +18,22 @@
 
 import android.os.Build
 import androidx.annotation.RequiresApi
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.CornerSize
 import androidx.compose.foundation.shape.CutCornerShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.testutils.assertContainsColor
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
@@ -361,6 +367,50 @@
     }
 
     @Test
+    fun button_animate_content_size_animates_height() {
+        val boxHeight = mutableStateOf(60.dp)
+        val frames = 14
+        val animationMillis = frames * 16
+        val buttonPadding = ButtonDefaults.ButtonVerticalPadding
+
+        rule.setContentWithTheme {
+            Button(onClick = {}, modifier = Modifier.testTag(TEST_TAG).fillMaxWidth()) {
+                Box(
+                    modifier =
+                        Modifier.animateContentSize(
+                                animationSpec = tween(animationMillis, easing = LinearEasing)
+                            )
+                            .fillMaxWidth()
+                            .requiredHeight(boxHeight.value)
+                ) {}
+            }
+        }
+        // Verify initial height
+        rule.onNodeWithTag(TEST_TAG).assertHeightIsEqualTo(60.dp + buttonPadding * 2)
+
+        // Set autoAdvance off to test the content size animation
+        rule.mainClock.autoAdvance = false
+        boxHeight.value = 100.dp
+        // Advance to the actual start of the animation
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+
+        // Advance to middle of animation
+        rule.mainClock.advanceTimeBy(animationMillis / 2L)
+        rule.waitForIdle()
+        // Verify that the animation is halfway finished
+        rule
+            .onNodeWithTag(TEST_TAG)
+            .assertHeightIsEqualTo(80.dp + buttonPadding * 2, tolerance = 2.dp)
+
+        // Set autoAdvance back on to finish the animation
+        rule.mainClock.autoAdvance = true
+        rule.waitForIdle()
+        // Verify end height is correct
+        rule.onNodeWithTag(TEST_TAG).assertHeightIsEqualTo(100.dp + buttonPadding * 2)
+    }
+
+    @Test
     fun has_icon_in_correct_location_for_three_slot_button_and_label_only() {
         val iconTag = "TestIcon"
         rule.setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
index d0333da..5b334ce 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
@@ -25,7 +25,6 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.sizeIn
@@ -180,9 +179,7 @@
 
 @Composable
 fun CenteredText(text: String) {
-    Column(modifier = Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
-        Text(text)
-    }
+    Column(verticalArrangement = Arrangement.Center) { Text(text) }
 }
 
 fun ComposeContentTestRule.setContentWithThemeForSizeAssertions(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollAwayTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollAwayTest.kt
index 90674cb..4849bdc 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollAwayTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollAwayTest.kt
@@ -48,6 +48,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.wear.compose.foundation.ScrollInfoProvider
+import androidx.wear.compose.foundation.curvedComposable
 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
@@ -207,7 +208,7 @@
                             )
                             .testTag(TIME_TEXT_TAG),
                 ) {
-                    composable { Box(Modifier.size(20.dp).background(timeTextColor)) }
+                    curvedComposable { Box(Modifier.size(20.dp).background(timeTextColor)) }
                 }
             }
         }
@@ -233,7 +234,7 @@
                             )
                             .testTag(TIME_TEXT_TAG)
                 ) {
-                    composable { Box(Modifier.size(20.dp).background(timeTextColor)) }
+                    curvedComposable { Box(Modifier.size(20.dp).background(timeTextColor)) }
                 }
                 LazyColumn(state = scrollState, modifier = Modifier.testTag(SCROLL_TAG)) {
                     item { ListHeader { Text("Buttons") } }
@@ -253,7 +254,6 @@
                         .testTag(TEST_TAG)
             ) {
                 TimeText(
-                    contentColor = timeTextColor,
                     modifier =
                         Modifier.scrollAway(
                                 scrollInfoProvider = ScrollInfoProvider(scrollState),
@@ -264,7 +264,7 @@
                             )
                             .testTag(TIME_TEXT_TAG)
                 ) {
-                    composable { Box(Modifier.size(20.dp).background(timeTextColor)) }
+                    curvedComposable { Box(Modifier.size(20.dp).background(timeTextColor)) }
                 }
                 Column(modifier = Modifier.verticalScroll(scrollState).testTag(SCROLL_TAG)) {
                     ListHeader { Text("Buttons") }
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
index 5c81306..526b932 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
@@ -29,23 +29,25 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.DeviceConfigurationOverride
 import androidx.compose.ui.test.ForcedSize
-import androidx.compose.ui.test.RoundScreen
 import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.then
+import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
-import com.google.testing.junit.testparameterinjector.TestParameter
-import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.CurvedScope
+import androidx.wear.compose.foundation.curvedComposable
+import androidx.wear.compose.foundation.weight
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TestName
 import org.junit.runner.RunWith
 
 @MediumTest
-@RunWith(TestParameterInjector::class)
+@RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
 class TimeTextScreenshotTest {
     @get:Rule val rule = createComposeRule()
@@ -60,60 +62,34 @@
         }
 
     @Test
-    fun time_text_with_clock_only_on_round_device() = verifyScreenshot {
+    fun time_text_with_clock_only() = verifyScreenshot {
         TimeText(
             modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
             timeSource = MockTimeSource,
-        ) {
-            time()
+        )
+    }
+
+    @Test
+    fun time_text_with_status() = verifyScreenshot {
+        TimeText(
+            modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
+            timeSource = MockTimeSource,
+        ) { time ->
+            curvedText("ETA 12:48")
+            timeTextSeparator()
+            curvedText(time)
         }
     }
 
     @Test
-    fun time_text_with_clock_only_on_non_round_device() =
-        verifyScreenshot(false) {
-            TimeText(
-                modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-                timeSource = MockTimeSource,
-            ) {
-                time()
-            }
-        }
-
-    @Test
-    fun time_text_with_status_on_round_device() = verifyScreenshot {
+    fun time_text_with_icon() = verifyScreenshot {
         TimeText(
             modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
             timeSource = MockTimeSource,
-        ) {
-            text("ETA 12:48")
-            separator()
-            time()
-        }
-    }
-
-    @Test
-    fun time_text_with_status_on_non_round_device() =
-        verifyScreenshot(false) {
-            TimeText(
-                modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-                timeSource = MockTimeSource,
-            ) {
-                text("ETA 12:48")
-                separator()
-                time()
-            }
-        }
-
-    @Test
-    fun time_text_with_icon_on_round_device() = verifyScreenshot {
-        TimeText(
-            modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-            timeSource = MockTimeSource,
-        ) {
-            time()
-            separator()
-            composable {
+        ) { time ->
+            curvedText(time)
+            timeTextSeparator()
+            curvedComposable {
                 Icon(
                     imageVector = Icons.Filled.Favorite,
                     contentDescription = "Favorite",
@@ -124,193 +100,126 @@
     }
 
     @Test
-    fun time_text_with_icon_on_non_round_device() =
-        verifyScreenshot(false) {
-            TimeText(
-                modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-                timeSource = MockTimeSource,
-            ) {
-                time()
-                separator()
-                composable {
-                    Icon(
-                        imageVector = Icons.Filled.Favorite,
-                        contentDescription = "Favorite",
-                        modifier = Modifier.size(13.dp)
-                    )
-                }
-            }
-        }
-
-    @Test
-    fun time_text_with_custom_colors_on_round_device() = verifyScreenshot {
+    fun time_text_with_custom_colors() = verifyScreenshot {
         val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
         val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
         val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
         TimeText(
-            contentColor = Color.Green,
-            timeTextStyle = timeTextStyle,
             modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
             timeSource = MockTimeSource,
-        ) {
-            text("ETA", customStyle)
-            composable { Spacer(modifier = Modifier.size(4.dp)) }
-            text("12:48")
-            separator(separatorStyle)
-            time()
+        ) { time ->
+            curvedText("ETA 12:48", style = customStyle)
+            curvedComposable { Spacer(modifier = Modifier.size(4.dp)) }
+            curvedText("12:48", style = customStyle)
+            timeTextSeparator(separatorStyle)
+            curvedText(time, style = timeTextStyle)
         }
     }
 
     @Test
-    fun time_text_with_long_status_on_round_device() = verifyScreenshot {
+    fun time_text_with_long_status() = verifyScreenshot {
         val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Green)
         TimeText(
-            contentColor = Color.Green,
-            timeTextStyle = timeTextStyle,
             modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
             timeSource = MockTimeSource,
-        ) {
-            text("Long status that should be ellipsized.")
-            composable { Spacer(modifier = Modifier.size(4.dp)) }
-            time()
-        }
-    }
-
-    @Test
-    fun time_text_with_custom_colors_on_non_round_device() =
-        verifyScreenshot(false) {
-            val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
-            val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
-            val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
-            TimeText(
-                contentColor = Color.Green,
-                timeTextStyle = timeTextStyle,
-                modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-                timeSource = MockTimeSource,
-            ) {
-                text("ETA", customStyle)
-                composable { Spacer(modifier = Modifier.size(4.dp)) }
-                text("12:48")
-                separator(separatorStyle)
-                time()
-            }
-        }
-
-    @Test
-    fun time_text_with_very_long_text_on_round_device() =
-        verifyScreenshot(true) {
-            val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
-            val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
-            val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
-            TimeText(
-                contentColor = Color.Green,
-                timeTextStyle = timeTextStyle,
-                maxSweepAngle = 180f,
-                modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-                timeSource = MockTimeSource,
-            ) {
-                text(
-                    "Very long text to ensure we are respecting the maxSweep parameter",
-                    customStyle
-                )
-                separator(separatorStyle)
-                time()
-            }
-        }
-
-    @Test
-    fun time_text_with_very_long_text_non_round_device() =
-        verifyScreenshot(false) {
-            val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
-            val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
-            val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
-            TimeText(
-                contentColor = Color.Green,
-                timeTextStyle = timeTextStyle,
-                modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-                timeSource = MockTimeSource,
-            ) {
-                text(
-                    "Very long text to ensure we are not taking more than one line and " +
-                        "leaving room for the time",
-                    customStyle
-                )
-                separator(separatorStyle)
-                time()
-            }
-        }
-
-    @Test
-    fun time_text_with_very_long_text_smaller_angle_on_round_device() =
-        verifyScreenshot(true) {
-            val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
-            val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
-            val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
-            TimeText(
-                contentColor = Color.Green,
-                timeTextStyle = timeTextStyle,
-                maxSweepAngle = 90f,
-                modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-                timeSource = MockTimeSource,
-            ) {
-                text(
-                    "Very long text to ensure we are respecting the maxSweep parameter",
-                    customStyle
-                )
-                separator(separatorStyle)
-                time()
-            }
-        }
-
-    @Test
-    fun time_text_long_text_before_time(@TestParameter shape: ScreenShape) =
-        TimeTextWithDefaults(shape.isRound) {
-            text("Very long text to ensure we are respecting the weight parameter", weight = 1f)
-            separator()
-            time()
-            separator()
-            text("More")
-        }
-
-    @Test
-    fun time_text_long_text_after_time(@TestParameter shape: ScreenShape) =
-        TimeTextWithDefaults(shape.isRound) {
-            text("More")
-            separator()
-            time()
-            separator()
-            text("Very long text to ensure we are respecting the weight parameter", weight = 1f)
-        }
-
-    // This is to get better names, so it says 'round_device' instead of 'true'
-    enum class ScreenShape(val isRound: Boolean) {
-        ROUND_DEVICE(true),
-        SQUARE_DEVICE(false)
-    }
-
-    private fun TimeTextWithDefaults(isDeviceRound: Boolean, content: TimeTextScope.() -> Unit) =
-        verifyScreenshot(isDeviceRound) {
-            TimeText(
-                contentColor = Color.Green,
-                maxSweepAngle = 180f,
-                modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
-                timeSource = MockTimeSource,
-                content = content
+        ) { time ->
+            curvedText(
+                "Long status that should be ellipsized.",
+                CurvedModifier.weight(1f),
+                overflow = TextOverflow.Ellipsis,
+                style = timeTextStyle
             )
+            curvedComposable { Spacer(modifier = Modifier.size(4.dp)) }
+            curvedText(time, style = timeTextStyle)
         }
+    }
 
-    private fun verifyScreenshot(isDeviceRound: Boolean = true, content: @Composable () -> Unit) {
+    @Test
+    fun time_text_with_very_long_text() = verifyScreenshot {
+        val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
+        val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
+        val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+        TimeText(
+            maxSweepAngle = 180f,
+            modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
+            timeSource = MockTimeSource,
+        ) { time ->
+            curvedText(
+                "Very long text to ensure we are respecting the maxSweep parameter",
+                CurvedModifier.weight(1f),
+                overflow = TextOverflow.Ellipsis,
+                maxSweepAngle = 180f,
+                style = customStyle
+            )
+            timeTextSeparator(separatorStyle)
+            curvedText(time, style = timeTextStyle)
+        }
+    }
+
+    @Test
+    fun time_text_with_very_long_text_smaller_angle() = verifyScreenshot {
+        val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
+        val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
+        val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+        TimeText(
+            maxSweepAngle = 90f,
+            modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
+            timeSource = MockTimeSource,
+        ) { time ->
+            curvedText(
+                "Very long text to ensure we are respecting the maxSweep parameter",
+                CurvedModifier.weight(1f),
+                overflow = TextOverflow.Ellipsis,
+                style = customStyle
+            )
+            timeTextSeparator(separatorStyle)
+            curvedText(time, style = timeTextStyle)
+        }
+    }
+
+    @Test
+    fun time_text_long_text_before_time() = TimeTextWithDefaults { time ->
+        curvedText(
+            "Very long text to ensure we are respecting the weight parameter",
+            CurvedModifier.weight(1f),
+            overflow = TextOverflow.Ellipsis
+        )
+        timeTextSeparator()
+        curvedText(time)
+        timeTextSeparator()
+        curvedText("More")
+    }
+
+    @Test
+    fun time_text_long_text_after_time() = TimeTextWithDefaults { time ->
+        curvedText("More")
+        timeTextSeparator()
+        curvedText(time)
+        timeTextSeparator()
+        curvedText(
+            "Very long text to ensure we are respecting the weight parameter",
+            CurvedModifier.weight(1f),
+            overflow = TextOverflow.Ellipsis
+        )
+    }
+
+    private fun TimeTextWithDefaults(content: CurvedScope.(String) -> Unit) = verifyScreenshot {
+        TimeText(
+            maxSweepAngle = 180f,
+            modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
+            timeSource = MockTimeSource,
+            content = content
+        )
+    }
+
+    private fun verifyScreenshot(content: @Composable () -> Unit) {
         rule.verifyScreenshot(
-            // Valid characters for golden identifiers are [A-Za-z0-9_-]
-            // TestParameterInjector adds '[' + parameter_values + ']' to the test name.
-            methodName = testName.methodName.replace("[", "_").replace("]", ""),
+            methodName = testName.methodName,
             screenshotRule = screenshotRule,
             content = {
                 val screenSize = LocalContext.current.resources.configuration.smallestScreenWidthDp
                 DeviceConfigurationOverride(
-                    DeviceConfigurationOverride.ForcedSize(
-                        DpSize(screenSize.dp, screenSize.dp)
-                    ) then DeviceConfigurationOverride.RoundScreen(isDeviceRound)
+                    DeviceConfigurationOverride.ForcedSize(DpSize(screenSize.dp, screenSize.dp))
                 ) {
                     content()
                 }
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextTest.kt
index f932e4a..e8a7528 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextTest.kt
@@ -21,20 +21,13 @@
 import androidx.compose.runtime.Composable
 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.test.DeviceConfigurationOverride
-import androidx.compose.ui.test.RoundScreen
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.unit.sp
 import androidx.test.filters.SdkSuppress
+import androidx.wear.compose.foundation.curvedComposable
 import java.util.Calendar
 import java.util.Locale
 import java.util.TimeZone
@@ -47,101 +40,54 @@
 
     @Test
     fun supports_testtag() {
-        rule.setContentWithTheme { TimeText(modifier = Modifier.testTag(TEST_TAG)) { time() } }
+        rule.setContentWithTheme { TimeText(modifier = Modifier.testTag(TEST_TAG)) }
 
         rule.onNodeWithTag(TEST_TAG).assertExists()
     }
 
     @Test
-    fun shows_time_by_default_on_non_round_device() {
+    fun shows_time_by_default() {
         val timeText = "time"
 
         rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText(
-                    timeSource =
-                        object : TimeSource {
-                            @Composable override fun currentTime(): String = timeText
-                        },
-                )
-            }
+            TimeText(
+                timeSource =
+                    object : TimeSource {
+                        @Composable override fun currentTime(): String = timeText
+                    },
+            )
         }
 
         // Note that onNodeWithText doesn't work for curved text, so only testing for non-round.
-        rule.onNodeWithText(timeText).assertIsDisplayed()
+        rule.onNodeWithContentDescription(timeText).assertIsDisplayed()
     }
 
     @Test
-    fun updates_clock_when_source_changes_on_non_round_device() {
+    fun updates_clock_when_source_changes() {
         val timeState = mutableStateOf("Unchanged")
 
         rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText(
-                    modifier = Modifier.testTag(TEST_TAG),
-                    timeSource =
-                        object : TimeSource {
-                            @Composable override fun currentTime(): String = timeState.value
-                        },
-                ) {
-                    time()
-                }
-            }
+            TimeText(
+                modifier = Modifier.testTag(TEST_TAG),
+                timeSource =
+                    object : TimeSource {
+                        @Composable override fun currentTime(): String = timeState.value
+                    },
+            )
         }
         timeState.value = "Changed"
-        rule.onNodeWithText("Changed").assertIsDisplayed()
-    }
-
-    @Test
-    fun updates_clock_when_source_changes_on_round_device() {
-        val timeState = mutableStateOf("Unchanged")
-
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
-                TimeText(
-                    modifier = Modifier.testTag(TEST_TAG),
-                    timeSource =
-                        object : TimeSource {
-                            @Composable override fun currentTime(): String = timeState.value
-                        },
-                ) {
-                    time()
-                }
-            }
-        }
-        timeState.value = "Changed"
-        rule.waitForIdle()
         rule.onNodeWithContentDescription("Changed").assertIsDisplayed()
     }
 
     @Test
-    fun checks_status_displayed_on_non_round_device() {
+    fun checks_status_displayed() {
         val statusText = "Status"
 
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText {
-                    text(statusText)
-                    separator()
-                    time()
-                }
-            }
-        }
-
-        rule.onNodeWithText(statusText).assertIsDisplayed()
-    }
-
-    @Test
-    fun checks_status_displayed_on_round_device() {
-        val statusText = "Status"
-
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
-                TimeText {
-                    text(statusText)
-                    separator()
-                    time()
-                }
+        rule.setContentWithTheme() {
+            TimeText { time ->
+                curvedText(statusText)
+                timeTextSeparator()
+                curvedText(time)
             }
         }
 
@@ -149,54 +95,26 @@
     }
 
     @Test
-    fun checks_separator_displayed_on_non_round_device() {
+    fun checks_separator_displayed() {
         val statusText = "Status"
         val separatorText = "·"
 
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText {
-                    text(statusText)
-                    separator()
-                    time()
-                }
-            }
-        }
-
-        rule.onNodeWithText(separatorText).assertIsDisplayed()
-    }
-
-    @Test
-    fun checks_separator_displayed_on_round_device() {
-        val statusText = "Status"
-        val separatorText = "·"
-
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
-                TimeText {
-                    text(statusText)
-                    separator()
-                    time()
-                }
-            }
-        }
+        rule.setContentWithTheme { BasicTimeTextWithStatus(statusText) }
 
         rule.onNodeWithContentDescription(separatorText).assertIsDisplayed()
     }
 
     @Test
-    fun checks_composable_displayed_on_non_round_device() {
+    fun checks_composable_displayed() {
         rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText {
-                    time()
-                    separator()
-                    composable {
-                        Text(
-                            modifier = Modifier.testTag(TEST_TAG),
-                            text = "Compose",
-                        )
-                    }
+            TimeText { time ->
+                curvedText(time)
+                timeTextSeparator()
+                curvedComposable {
+                    Text(
+                        modifier = Modifier.testTag(TEST_TAG),
+                        text = "Compose",
+                    )
                 }
             }
         }
@@ -205,178 +123,6 @@
     }
 
     @Test
-    fun checks_composable_displayed_on_round_device() {
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
-                TimeText {
-                    time()
-                    separator()
-                    composable {
-                        Text(
-                            modifier = Modifier.testTag(TEST_TAG),
-                            text = "Compose",
-                        )
-                    }
-                }
-            }
-        }
-
-        rule.onNodeWithTag(TEST_TAG).assertIsDisplayed()
-    }
-
-    @Test
-    fun changes_timeTextStyle_on_non_round_device() {
-        val timeText = "testTime"
-
-        val testTextStyle =
-            TextStyle(color = Color.Green, background = Color.Black, fontSize = 20.sp)
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText(
-                    timeSource =
-                        object : TimeSource {
-                            @Composable override fun currentTime(): String = timeText
-                        },
-                    timeTextStyle = testTextStyle
-                ) {
-                    time()
-                }
-            }
-        }
-        val actualStyle = rule.textStyleOf(timeText)
-        Assert.assertEquals(testTextStyle.color, actualStyle.color)
-        Assert.assertEquals(testTextStyle.background, actualStyle.background)
-        Assert.assertEquals(testTextStyle.fontSize, actualStyle.fontSize)
-    }
-
-    @Test
-    fun changes_material_theme_on_non_round_device_except_color() {
-        val timeText = "testTime"
-
-        val testTextStyle =
-            TextStyle(
-                color = Color.Green,
-                background = Color.Black,
-                fontStyle = FontStyle.Italic,
-                fontSize = 25.sp,
-                fontFamily = FontFamily.SansSerif
-            )
-        rule.setContent {
-            MaterialTheme(typography = MaterialTheme.typography.copy(arcMedium = testTextStyle)) {
-                DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                    TimeText(
-                        timeSource =
-                            object : TimeSource {
-                                @Composable override fun currentTime(): String = timeText
-                            },
-                    ) {
-                        time()
-                    }
-                }
-            }
-        }
-        val actualStyle = rule.textStyleOf(timeText)
-        Assert.assertEquals(testTextStyle.background, actualStyle.background)
-        Assert.assertEquals(testTextStyle.fontSize, actualStyle.fontSize)
-        Assert.assertEquals(testTextStyle.fontStyle, actualStyle.fontStyle)
-        Assert.assertEquals(testTextStyle.fontFamily, actualStyle.fontFamily)
-        Assert.assertNotEquals(testTextStyle.color, actualStyle.color)
-    }
-
-    @Test
-    fun color_remains_onBackground_when_material_theme_changed_on_non_round_device() {
-        val timeText = "testTime"
-        var onBackgroundColor = Color.Unspecified
-
-        val testTextStyle =
-            TextStyle(
-                color = Color.Green,
-                background = Color.Black,
-                fontStyle = FontStyle.Italic,
-                fontSize = 25.sp,
-                fontFamily = FontFamily.SansSerif
-            )
-        rule.setContent {
-            MaterialTheme(typography = MaterialTheme.typography.copy(labelSmall = testTextStyle)) {
-                onBackgroundColor = MaterialTheme.colorScheme.onBackground
-                DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                    TimeText(
-                        timeSource =
-                            object : TimeSource {
-                                @Composable override fun currentTime(): String = timeText
-                            },
-                    ) {
-                        time()
-                    }
-                }
-            }
-        }
-        val actualStyle = rule.textStyleOf(timeText)
-        Assert.assertEquals(onBackgroundColor, actualStyle.color)
-    }
-
-    @Test
-    fun has_correct_default_leading_text_color_on_non_round_device() {
-        val leadingText = "leadingText"
-        var primaryColor = Color.Unspecified
-
-        rule.setContentWithTheme {
-            primaryColor = MaterialTheme.colorScheme.primary
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText {
-                    text(leadingText)
-                    separator()
-                    time()
-                }
-            }
-        }
-        val actualStyle = rule.textStyleOf(leadingText)
-        Assert.assertEquals(primaryColor, actualStyle.color)
-    }
-
-    @Test
-    fun supports_custom_leading_text_color_on_non_round_device() {
-        val leadingText = "leadingText"
-        val customColor = Color.Green
-
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText(
-                    contentColor = customColor,
-                ) {
-                    text(leadingText)
-                    separator()
-                    time()
-                }
-            }
-        }
-        val actualStyle = rule.textStyleOf(leadingText)
-        Assert.assertEquals(customColor, actualStyle.color)
-    }
-
-    @Test
-    fun supports_custom_text_style_on_non_round_device() {
-        val leadingText = "leadingText"
-
-        val timeTextStyle = TextStyle(background = Color.Blue, fontSize = 14.sp)
-        val contentTextStyle =
-            TextStyle(color = Color.Green, background = Color.Black, fontSize = 20.sp)
-        rule.setContentWithTheme {
-            DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(false)) {
-                TimeText(contentColor = Color.Red, timeTextStyle = timeTextStyle) {
-                    text(leadingText, contentTextStyle)
-                    separator()
-                    time()
-                }
-            }
-        }
-        val actualStyle = rule.textStyleOf(leadingText)
-        Assert.assertEquals(contentTextStyle.color, actualStyle.color)
-        Assert.assertEquals(contentTextStyle.background, actualStyle.background)
-        Assert.assertEquals(contentTextStyle.fontSize, actualStyle.fontSize)
-    }
-
-    @Test
     fun formats_current_time() {
         val currentTimeInMillis = 1631544258000L // 2021-09-13 14:44:18
         val format = "HH:mm:ss"
@@ -418,4 +164,13 @@
         }
         Assert.assertEquals(expectedTime, actualTime)
     }
+
+    @Composable
+    private fun BasicTimeTextWithStatus(statusText: String) {
+        TimeText { time ->
+            curvedText(statusText)
+            timeTextSeparator()
+            curvedText(time)
+        }
+    }
 }
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AppScaffold.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AppScaffold.kt
index edffe7e..51fc42b 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AppScaffold.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AppScaffold.kt
@@ -46,7 +46,7 @@
 @Composable
 public fun AppScaffold(
     modifier: Modifier = Modifier,
-    timeText: @Composable () -> Unit = { TimeText { time() } },
+    timeText: @Composable () -> Unit = { TimeText() },
     content: @Composable BoxScope.() -> Unit,
 ) {
     CompositionLocalProvider(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index b3a9562..dcdb65b 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -30,7 +30,6 @@
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
@@ -1826,7 +1825,7 @@
 
 @Composable
 private fun Modifier.buttonSizeModifier(): Modifier =
-    this.defaultMinSize(minHeight = ButtonDefaults.Height).height(IntrinsicSize.Min)
+    this.defaultMinSize(minHeight = ButtonDefaults.Height)
 
 @Composable
 private fun Modifier.compactButtonModifier(): Modifier =
@@ -1858,7 +1857,6 @@
         // want them to be able to fit their content
         modifier =
             modifier
-                .fillMaxHeight()
                 .width(intrinsicSize = IntrinsicSize.Max)
                 .container(colors.containerPainter(enabled = enabled), shape, border)
                 .combinedClickable(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ButtonGroup.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ButtonGroup.kt
index ebfc393f..d42dcc10 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ButtonGroup.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ButtonGroup.kt
@@ -21,7 +21,7 @@
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
@@ -101,7 +101,7 @@
                 return this.then(ButtonGroupElement(minWidth = minWidth))
             }
 
-            override fun Modifier.enlargeOnPress(interactionSource: MutableInteractionSource) =
+            override fun Modifier.animateWidth(interactionSource: InteractionSource) =
                 this.then(
                     EnlargeOnPressElement(
                         interactionSource = interactionSource,
@@ -206,13 +206,13 @@
      * @param minWidth the minimum width. If none is specified, minimumInteractiveComponentSize is
      *   used.
      */
-    public fun Modifier.minWidth(minWidth: Dp = minimumInteractiveComponentSize): Modifier
+    public fun Modifier.minWidth(minWidth: Dp = ButtonGroupDefaults.MinWidth): Modifier
 
     /**
      * Specifies the interaction source to use with this item. This is used to listen to events and
-     * grow/shrink the buttons in reaction.
+     * animate growing the pressed button and shrink the neighbor(s).
      */
-    public fun Modifier.enlargeOnPress(interactionSource: MutableInteractionSource): Modifier
+    public fun Modifier.animateWidth(interactionSource: InteractionSource): Modifier
 }
 
 /** Contains the default values used by [ButtonGroup] */
@@ -236,6 +236,9 @@
     /** Spacing between buttons. */
     public val Spacing: Dp = 4.dp
 
+    /** Default for the minimum width of buttons in a ButtonGroup */
+    public val MinWidth: Dp = minimumInteractiveComponentSize
+
     /** Padding at each side of the [ButtonGroup], as a percentage of the available space. */
     private const val FullWidthHorizontalPaddingPercentage: Float = 5.2f
 }
@@ -303,7 +306,7 @@
 }
 
 internal class EnlargeOnPressElement(
-    val interactionSource: MutableInteractionSource,
+    val interactionSource: InteractionSource,
     val downAnimSpec: AnimationSpec<Float>,
     val upAnimSpec: AnimationSpec<Float>,
 ) : ModifierNodeElement<EnlargeOnPressNode>() {
@@ -341,7 +344,7 @@
 }
 
 internal class EnlargeOnPressNode(
-    var interactionSource: MutableInteractionSource,
+    var interactionSource: InteractionSource,
     var downAnimSpec: AnimationSpec<Float>,
     var upAnimSpec: AnimationSpec<Float>,
 ) : ParentDataModifierNode, Modifier.Node() {
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Container.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Container.kt
index 74c6450..826394a 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Container.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Container.kt
@@ -20,15 +20,25 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.paint
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.translate
 import androidx.compose.ui.graphics.painter.ColorPainter
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.times
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastIsFinite
+import androidx.compose.ui.util.fastRoundToInt
 import androidx.wear.compose.foundation.LocalReduceMotion
 import androidx.wear.compose.foundation.lazy.LocalTransformingLazyColumnItemScope
 import androidx.wear.compose.material3.lazy.scrollTransform
@@ -65,7 +75,7 @@
     return itemScope?.let { tlcScope -> scrollTransform(tlcScope, shape, painter, border) }
         ?: borderModifier
             .clip(shape = shape)
-            .paint(painter = painter, contentScale = ContentScale.Crop)
+            .paintBackground(painter = painter, contentScale = ContentScale.Crop)
 }
 
 /**
@@ -101,3 +111,97 @@
         scrollTransform(tlcScope, shape, ColorPainter(color), border)
     } ?: borderModifier.clip(shape = shape).background(color = color)
 }
+
+/**
+ * Paint the background using [Painter].
+ *
+ * This modifier simply paints the background, without modifying the size.
+ *
+ * @param painter [Painter] to be drawn by this [Modifier]
+ * @param alignment specifies alignment of the [painter] relative to content
+ * @param contentScale strategy for scaling [painter] if its size does not match the content size
+ */
+internal fun Modifier.paintBackground(
+    painter: Painter,
+    alignment: Alignment = Alignment.Center,
+    contentScale: ContentScale = ContentScale.Inside,
+) = this then PainterElement(painter = painter, alignment = alignment, contentScale = contentScale)
+
+private data class PainterElement(
+    val painter: Painter,
+    val contentScale: ContentScale,
+    var alignment: Alignment = Alignment.Center,
+) : ModifierNodeElement<PainterNode>() {
+    override fun create(): PainterNode {
+        return PainterNode(painter = painter, alignment = alignment, contentScale = contentScale)
+    }
+
+    override fun update(node: PainterNode) {
+        node.painter = painter
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "paint"
+        properties["painter"] = painter
+        properties["alignment"] = alignment
+        properties["contentScale"] = contentScale
+    }
+}
+
+private class PainterNode(
+    var painter: Painter,
+    var alignment: Alignment = Alignment.Center,
+    val contentScale: ContentScale,
+) : Modifier.Node(), DrawModifierNode {
+
+    override fun ContentDrawScope.draw() {
+        val intrinsicSize = painter.intrinsicSize
+        val srcWidth =
+            if (intrinsicSize.hasSpecifiedAndFiniteWidth()) {
+                intrinsicSize.width
+            } else {
+                size.width
+            }
+
+        val srcHeight =
+            if (intrinsicSize.hasSpecifiedAndFiniteHeight()) {
+                intrinsicSize.height
+            } else {
+                size.height
+            }
+
+        val srcSize = Size(srcWidth, srcHeight)
+        val scaledSize =
+            if (size.width != 0f && size.height != 0f) {
+                srcSize * contentScale.computeScaleFactor(srcSize, size)
+            } else {
+                Size.Zero
+            }
+
+        val alignedPosition =
+            alignment.align(
+                IntSize(scaledSize.width.fastRoundToInt(), scaledSize.height.fastRoundToInt()),
+                IntSize(size.width.fastRoundToInt(), size.height.fastRoundToInt()),
+                layoutDirection
+            )
+
+        val dx = alignedPosition.x.toFloat()
+        val dy = alignedPosition.y.toFloat()
+
+        translate(dx, dy) { with(painter) { draw(size = scaledSize) } }
+
+        // Maintain the same pattern as Modifier.drawBehind to allow chaining of DrawModifiers
+        drawContent()
+    }
+
+    private fun Size.hasSpecifiedAndFiniteWidth() = this != Size.Unspecified && width.fastIsFinite()
+
+    private fun Size.hasSpecifiedAndFiniteHeight() =
+        this != Size.Unspecified && height.fastIsFinite()
+
+    override fun toString(): String =
+        "PainterModifier(" +
+            "painter=$painter, " +
+            "alignment=$alignment, " +
+            "contentScale=$contentScale)"
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
index f95bd3c..351a47c 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
@@ -22,17 +22,8 @@
 import android.content.IntentFilter
 import android.text.format.DateFormat
 import androidx.annotation.VisibleForTesting
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
@@ -42,17 +33,14 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.StrokeCap
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
 import androidx.wear.compose.foundation.ArcPaddingValues
 import androidx.wear.compose.foundation.CurvedAlignment
 import androidx.wear.compose.foundation.CurvedDirection
@@ -61,35 +49,29 @@
 import androidx.wear.compose.foundation.CurvedScope
 import androidx.wear.compose.foundation.CurvedTextStyle
 import androidx.wear.compose.foundation.background
-import androidx.wear.compose.foundation.curvedComposable
+import androidx.wear.compose.foundation.basicCurvedText
 import androidx.wear.compose.foundation.curvedRow
 import androidx.wear.compose.foundation.padding
 import androidx.wear.compose.foundation.sizeIn
-import androidx.wear.compose.foundation.weight
-import androidx.wear.compose.material3.TimeTextDefaults.CurvedTextSeparator
-import androidx.wear.compose.material3.TimeTextDefaults.TextSeparator
 import androidx.wear.compose.material3.TimeTextDefaults.timeFormat
+import androidx.wear.compose.material3.TimeTextDefaults.timeTextStyle
 import androidx.wear.compose.materialcore.currentTimeMillis
 import androidx.wear.compose.materialcore.is24HourFormat
-import androidx.wear.compose.materialcore.isRoundDevice
 import java.util.Calendar
 import java.util.Locale
 
 /**
- * Layout to show the current time and a label at the top of the screen. If device has a round
- * screen, then the time will be curved along the top edge of the screen, if rectangular - then the
- * text and the time will be straight.
+ * Layout to show the current time and a label, they will be drawn in a curve, following the top
+ * edge of the screen.
  *
  * Note that Wear Material UX guidance recommends that time text should not be larger than
- * [TimeTextDefaults.MaxSweepAngle] of the screen edge on round devices, which is enforced by
- * default. It is recommended that additional content, if any, is limited to short status messages
- * before the [TimeTextScope.time] using the MaterialTheme.colorScheme.primary color.
+ * [TimeTextDefaults.MaxSweepAngle] of the screen edge, which is enforced by default. It is
+ * recommended that additional content, if any, is limited to short status messages before the time
+ * using the MaterialTheme.colorScheme.primary color.
  *
  * For more information, see the
  * [Curved Text](https://developer.android.com/training/wearables/components/curved-text) guide.
  *
- * Different components of [TimeText] can be added through methods of [TimeTextScope].
- *
  * A simple [TimeText] which shows the current time:
  *
  * @sample androidx.wear.compose.material3.samples.TimeTextClockOnly
@@ -97,15 +79,19 @@
  * A [TimeText] with a short app status message shown:
  *
  * @sample androidx.wear.compose.material3.samples.TimeTextWithStatus
+ *
+ * A [TimeText] with a long status message, that needs ellipsizing:
+ *
+ * @sample androidx.wear.compose.material3.samples.TimeTextWithStatusEllipsized
  * @param modifier The modifier to be applied to the component.
  * @param curvedModifier The [CurvedModifier] used to restrict the arc in which [TimeText] is drawn.
  * @param maxSweepAngle The default maximum sweep angle in degrees.
  * @param timeSource [TimeSource] which retrieves the current time and formats it.
- * @param timeTextStyle [TextStyle] for the time text itself.
- * @param contentColor [Color] of content of displayed through [TimeTextScope.text] and
- *   [TimeTextScope.composable].
  * @param contentPadding The spacing values between the container and the content.
- * @param content The content of the [TimeText] - displays the current time by default.
+ * @param content The content of the [TimeText] - displays the current time by default. This lambda
+ *   receives the current time as a String and should display it using curvedText. Note that if long
+ *   curved text is included here, it should specify [CurvedModifier.weight] on it so that the space
+ *   available is suitably allocated.
  */
 @Composable
 public fun TimeText(
@@ -113,91 +99,31 @@
     curvedModifier: CurvedModifier = CurvedModifier,
     maxSweepAngle: Float = TimeTextDefaults.MaxSweepAngle,
     timeSource: TimeSource = TimeTextDefaults.rememberTimeSource(timeFormat()),
-    timeTextStyle: TextStyle = TimeTextDefaults.timeTextStyle(),
-    contentColor: Color = MaterialTheme.colorScheme.primary,
     contentPadding: PaddingValues = TimeTextDefaults.ContentPadding,
-    content: TimeTextScope.() -> Unit = { time() }
+    content: CurvedScope.(String) -> Unit = { time -> timeTextCurvedText(time) }
 ) {
-    val timeText = timeSource.currentTime()
+    val currentTime = timeSource.currentTime()
     val backgroundColor = CurvedTextDefaults.backgroundColor()
 
-    if (isRoundDevice()) {
-        CurvedLayout(modifier = modifier) {
-            curvedRow(
-                modifier =
-                    curvedModifier
-                        .sizeIn(maxSweepDegrees = maxSweepAngle)
-                        .padding(contentPadding.toArcPadding())
-                        .background(backgroundColor, StrokeCap.Round),
-                radialAlignment = CurvedAlignment.Radial.Center
-            ) {
-                CurvedTimeTextScope(timeText, timeTextStyle, maxSweepAngle, contentColor).apply {
-                    content()
-                    Show()
-                }
-            }
-        }
-    } else {
-        Box(modifier.fillMaxSize()) {
-            Row(
-                modifier =
-                    Modifier.align(Alignment.TopCenter)
-                        .background(backgroundColor, CircleShape)
-                        .padding(contentPadding),
-                verticalAlignment = Alignment.CenterVertically,
-                horizontalArrangement = Arrangement.Center
-            ) {
-                LinearTimeTextScope(timeText, timeTextStyle, contentColor).apply {
-                    content()
-                    Show()
-                }
-            }
+    CurvedLayout(modifier = modifier) {
+        curvedRow(
+            modifier =
+                curvedModifier
+                    .sizeIn(maxSweepDegrees = maxSweepAngle)
+                    .padding(contentPadding.toArcPadding())
+                    .background(backgroundColor, StrokeCap.Round),
+            radialAlignment = CurvedAlignment.Radial.Center
+        ) {
+            content(currentTime)
         }
     }
 }
 
-/** Receiver scope which is used by [TimeText]. */
-public sealed class TimeTextScope {
-    /**
-     * Adds a composable [Text] for non-round devices and [curvedText] for round devices to
-     * [TimeText] content. Typically used to add a short status message ahead of the time text.
-     *
-     * @param text The text to display.
-     * @param style configuration for the [text] such as color, font etc.
-     * @param weight Size the text's width proportional to its weight relative to other weighted
-     *   sibling elements in the TimeText. Specify NaN to make this text not have a weight
-     *   specified. The default value, [TimeTextDefaults.AutoTextWeight], makes this text have
-     *   weight 1f if it's the only one, and not have weight if there are two or more.
-     */
-    public abstract fun text(
-        text: String,
-        style: TextStyle? = null,
-        weight: Float = TimeTextDefaults.AutoTextWeight
-    )
-
-    /** Adds a text displaying current time. */
-    public abstract fun time()
-
-    /**
-     * Adds a separator in [TimeText].
-     *
-     * @param style configuration for the [separator] such as color, font etc.
-     */
-    public abstract fun separator(style: TextStyle? = null)
-
-    /**
-     * Adds a composable in content of [TimeText]. This can be used to display non-text information
-     * such as an icon.
-     *
-     * @param content Slot for the [composable] to be displayed.
-     */
-    public abstract fun composable(content: @Composable () -> Unit)
-}
-
 /** Contains the default values used by [TimeText]. */
 public object TimeTextDefaults {
     /** The default padding from the edge of the screen. */
     private val Padding = PaddingDefaults.edgePadding
+
     /** Default format for 24h clock. */
     public const val TimeFormat24Hours: String = "HH:mm"
 
@@ -228,8 +154,8 @@
     }
 
     /**
-     * Creates a [TextStyle] with default parameters used for showing time on square screens. By
-     * default a copy of MaterialTheme.typography.arcMedium style is created.
+     * Creates a [CurvedTextStyle] with default parameters used for showing time. By default a copy
+     * of MaterialTheme.typography.arcMedium style is created.
      *
      * @param background The background color.
      * @param color The main color.
@@ -240,9 +166,11 @@
         background: Color = Color.Unspecified,
         color: Color = MaterialTheme.colorScheme.onBackground,
         fontSize: TextUnit = TextUnit.Unspecified,
-    ): TextStyle =
-        MaterialTheme.typography.arcMedium +
-            TextStyle(color = color, background = background, fontSize = fontSize)
+    ): CurvedTextStyle =
+        CurvedTextStyle(
+            MaterialTheme.typography.arcMedium +
+                TextStyle(color = color, background = background, fontSize = fontSize)
+        )
 
     /**
      * Creates a default implementation of [TimeSource] and remembers it. Once the system time
@@ -260,48 +188,37 @@
     @Composable
     public fun rememberTimeSource(timeFormat: String): TimeSource =
         remember(timeFormat) { DefaultTimeSource(timeFormat) }
+}
 
-    /**
-     * A default implementation of Separator shown between any text/composable and the time on
-     * non-round screens.
-     *
-     * @param modifier A default modifier for the separator.
-     * @param textStyle A [TextStyle] for the separator.
-     * @param contentPadding The spacing values between the container and the separator.
-     */
-    @Composable
-    internal fun TextSeparator(
-        modifier: Modifier = Modifier,
-        textStyle: TextStyle = timeTextStyle(),
-        contentPadding: PaddingValues = PaddingValues(horizontal = 4.dp)
+/**
+ * Default curved text to use in a [TimeText], for displaying the time
+ *
+ * @param time The time to display.
+ * @param style A [CurvedTextStyle] to override the style used.
+ */
+public fun CurvedScope.timeTextCurvedText(time: String, style: CurvedTextStyle? = null) {
+    basicCurvedText(
+        time,
     ) {
-        Text(text = "·", style = textStyle, modifier = modifier.padding(contentPadding))
+        style?.let { timeTextStyle() + it } ?: timeTextStyle()
     }
+}
 
-    /**
-     * A default implementation of Separator shown between any text/composable and the time on round
-     * screens.
-     *
-     * @param curvedTextStyle A [CurvedTextStyle] for the separator.
-     * @param contentArcPadding [ArcPaddingValues] for the separator text.
-     */
-    internal fun CurvedScope.CurvedTextSeparator(
-        curvedTextStyle: CurvedTextStyle? = null,
-        contentArcPadding: ArcPaddingValues = ArcPaddingValues(angular = 4.dp)
-    ) {
-        curvedText(
-            text = "·",
-            style = curvedTextStyle,
-            modifier = CurvedModifier.padding(contentArcPadding)
-        )
-    }
-
-    /**
-     * Weight value used to specify that the value is automatic. It will be 1f when there is one
-     * text, and no weight will be used if there are 2 or more texts. For the 2+ texts case, usually
-     * one of them should have weight manually specified to ensure its properly cut and ellipsized.
-     */
-    public val AutoTextWeight: Float = -1f
+/**
+ * A default implementation of Separator, to be shown between any text/composable and the time.
+ *
+ * @param curvedTextStyle A [CurvedTextStyle] for the separator.
+ * @param contentArcPadding [ArcPaddingValues] for the separator text.
+ */
+public fun CurvedScope.timeTextSeparator(
+    curvedTextStyle: CurvedTextStyle? = null,
+    contentArcPadding: ArcPaddingValues = ArcPaddingValues(angular = 4.dp)
+) {
+    curvedText(
+        text = "·",
+        style = curvedTextStyle,
+        modifier = CurvedModifier.padding(contentArcPadding)
+    )
 }
 
 public interface TimeSource {
@@ -314,122 +231,6 @@
     @Composable public fun currentTime(): String
 }
 
-/** Implementation of [TimeTextScope] for round devices. */
-internal class CurvedTimeTextScope(
-    private val timeText: String,
-    private val timeTextStyle: TextStyle,
-    private val maxSweepAngle: Float,
-    contentColor: Color,
-) : TimeTextScope() {
-    private var textCount = 0
-    private val pending = mutableListOf<CurvedScope.() -> Unit>()
-    private val contentTextStyle = timeTextStyle.merge(contentColor)
-
-    override fun text(text: String, style: TextStyle?, weight: Float) {
-        textCount++
-        pending.add {
-            curvedText(
-                text = text,
-                overflow = TextOverflow.Ellipsis,
-                maxSweepAngle = maxSweepAngle,
-                style = CurvedTextStyle(style = contentTextStyle.merge(style)),
-                modifier =
-                    if (weight.isValidWeight()) CurvedModifier.weight(weight)
-                    // Note that we are creating a lambda here, but textCount is actually read
-                    // later, during the call to Show, when the pending list is fully constructed.
-                    else if (weight == TimeTextDefaults.AutoTextWeight && textCount <= 1)
-                        CurvedModifier.weight(1f)
-                    else CurvedModifier
-            )
-        }
-    }
-
-    override fun time() {
-        pending.add {
-            curvedText(
-                timeText,
-                maxSweepAngle = maxSweepAngle,
-                style = CurvedTextStyle(timeTextStyle)
-            )
-        }
-    }
-
-    override fun separator(style: TextStyle?) {
-        pending.add { CurvedTextSeparator(CurvedTextStyle(style = timeTextStyle.merge(style))) }
-    }
-
-    override fun composable(content: @Composable () -> Unit) {
-        pending.add {
-            curvedComposable {
-                CompositionLocalProvider(
-                    LocalContentColor provides contentTextStyle.color,
-                    LocalTextStyle provides contentTextStyle,
-                    content = content
-                )
-            }
-        }
-    }
-
-    fun CurvedScope.Show() {
-        pending.fastForEach { it() }
-    }
-}
-
-/** Implementation of [TimeTextScope] for non-round devices. */
-internal class LinearTimeTextScope(
-    private val timeText: String,
-    private val timeTextStyle: TextStyle,
-    contentColor: Color,
-) : TimeTextScope() {
-    private var textCount = 0
-    private val pending = mutableListOf<@Composable RowScope.() -> Unit>()
-    private val contentTextStyle = timeTextStyle.merge(contentColor)
-
-    override fun text(text: String, style: TextStyle?, weight: Float) {
-        textCount++
-        pending.add {
-            Text(
-                text = text,
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
-                style = contentTextStyle.merge(style),
-                modifier =
-                    if (weight.isValidWeight()) Modifier.weight(weight, fill = false)
-                    // Note that we are creating a lambda here, but textCount is actually read
-                    // later, during the call to Show, when the pending list is fully constructed.
-                    else if (weight == TimeTextDefaults.AutoTextWeight && textCount <= 1)
-                        Modifier.weight(1f, fill = false)
-                    else Modifier
-            )
-        }
-    }
-
-    override fun time() {
-        pending.add { Text(timeText, style = timeTextStyle) }
-    }
-
-    override fun separator(style: TextStyle?) {
-        pending.add { TextSeparator(textStyle = timeTextStyle.merge(style)) }
-    }
-
-    override fun composable(content: @Composable () -> Unit) {
-        pending.add {
-            CompositionLocalProvider(
-                LocalContentColor provides contentTextStyle.color,
-                LocalTextStyle provides contentTextStyle,
-                content = content
-            )
-        }
-    }
-
-    @Composable
-    fun RowScope.Show() {
-        pending.fastForEach { it() }
-    }
-}
-
-private fun Float.isValidWeight() = !isNaN() && this > 0f
-
 internal class DefaultTimeSource(timeFormat: String) : TimeSource {
     private val _timeFormat = timeFormat
 
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/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index a674c88..685cf6d 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -109,7 +109,7 @@
         @Composable { it: @Composable () -> Unit ->
             // Only material3 demos benefit from the Material3 ScreenScaffold
             if (category.materialVersion == 3) {
-                val timeText = @Composable { androidx.wear.compose.material3.TimeText { time() } }
+                val timeText = @Composable { androidx.wear.compose.material3.TimeText() }
                 androidx.wear.compose.material3.ScreenScaffold(
                     scrollState = state,
                     timeText = remember { timeText },
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index 927217c..68bb715 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -49,6 +49,7 @@
     implementation(project(":compose:foundation:foundation-layout"))
     implementation(project(":compose:runtime:runtime"))
     implementation(project(":wear:compose:compose-material"))
+    implementation(project(":wear:compose:compose-material3"))
     implementation(project(":wear:compose:compose-foundation"))
     implementation(project(":wear:compose:compose-foundation-samples"))
     implementation(project(":wear:compose:compose-material-samples"))
diff --git a/wear/compose/integration-tests/navigation/src/main/java/androidx/wear/compose/integration/navigation/MainActivity.kt b/wear/compose/integration-tests/navigation/src/main/java/androidx/wear/compose/integration/navigation/MainActivity.kt
index fe2e2ee..5e56425 100644
--- a/wear/compose/integration-tests/navigation/src/main/java/androidx/wear/compose/integration/navigation/MainActivity.kt
+++ b/wear/compose/integration-tests/navigation/src/main/java/androidx/wear/compose/integration/navigation/MainActivity.kt
@@ -32,10 +32,14 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.edgeSwipeToDismiss
+import androidx.wear.compose.foundation.pager.rememberPagerState
 import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.material.CompactChip
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.Text
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.HorizontalPagerScaffold
+import androidx.wear.compose.material3.ScreenScaffold
 import androidx.wear.compose.navigation.SwipeDismissableNavHost
 import androidx.wear.compose.navigation.composable
 import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
@@ -71,6 +75,11 @@
                                 onClick = { navController.navigate(EDGE_SWIPE_SCREEN) },
                                 label = { Text("Screen with edge swipe") },
                             )
+                            Spacer(modifier = Modifier.fillMaxWidth().height(4.dp))
+                            CompactChip(
+                                onClick = { navController.navigate(PAGER_SCAFFOLD_SCREEN) },
+                                label = { Text("Screen with PagerScaffold") },
+                            )
                         }
                     }
                     composable(SCREEN2) {
@@ -127,6 +136,22 @@
                             )
                         }
                     }
+                    composable(PAGER_SCAFFOLD_SCREEN) {
+                        AppScaffold {
+                            val pagerState = rememberPagerState(pageCount = { 10 })
+
+                            HorizontalPagerScaffold(pagerState = pagerState) { page ->
+                                ScreenScaffold {
+                                    Box(
+                                        modifier = Modifier.fillMaxSize(),
+                                        contentAlignment = Alignment.Center
+                                    ) {
+                                        Text("Page $page")
+                                    }
+                                }
+                            }
+                        }
+                    }
                 }
             }
         }
@@ -137,3 +162,4 @@
 private const val SCREEN2 = "screen2"
 private const val SCREEN3 = "screen3"
 private const val EDGE_SWIPE_SCREEN = "edge_swipe_screen"
+private const val PAGER_SCAFFOLD_SCREEN = "pager_scaffold_screen"
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..9763e74 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 @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @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();
+  }
+
+  @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @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..9763e74 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 @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @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();
+  }
+
+  @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @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..99e14b8 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,16 @@
             }
         }
 
+        /** Gets the trigger to start the animation. */
+        @OptIn(markerClass = ProtoLayoutExperimental.class)
+        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 +550,8 @@
                     + getRawResourceId()
                     + ", progress="
                     + getProgress()
+                    + ", startTrigger="
+                    + getStartTrigger()
                     + "}";
         }
 
@@ -590,6 +602,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..1c7a82a 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
@@ -23,6 +23,7 @@
 import androidx.wear.protolayout.expression.DynamicBuilders;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool;
 import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental;
 import androidx.wear.protolayout.expression.RequiresSchemaVersion;
 import androidx.wear.protolayout.proto.TriggerProto;
 
@@ -48,6 +49,162 @@
         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)
+    @ProtoLayoutExperimental
+    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)
+    @ProtoLayoutExperimental
+    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 {
@@ -223,8 +380,15 @@
 
     /** Creates a new wrapper instance from the proto. */
     @RestrictTo(Scope.LIBRARY_GROUP)
+    @ProtoLayoutExperimental
     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);
         }
@@ -234,6 +398,7 @@
         throw new IllegalStateException("Proto was not a recognised instance of Trigger");
     }
 
+    @ProtoLayoutExperimental
     static @NonNull Trigger triggerFromProto(TriggerProto.@NonNull Trigger proto) {
         return triggerFromProto(proto, null);
     }
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);
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
index d8422e1..3b9f2f0 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
+++ b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
@@ -32,17 +32,17 @@
                 )
         }
 
-        fun assumeVendorApiLevel(level: Int) {
+        fun assumeWindowExtensionVersionEquals(level: Int) {
             val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
             assumeTrue(apiLevel == level)
         }
 
-        fun assumeAtLeastVendorApiLevel(min: Int) {
+        fun assumeAtLeastWindowExtensionVersion(min: Int) {
             val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
             assumeTrue(apiLevel >= min)
         }
 
-        fun assumeBeforeVendorApiLevel(max: Int) {
+        fun assumeBeforeWindowExtensionVersion(max: Int) {
             val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
             assumeTrue(apiLevel < max)
             assumeTrue(apiLevel > 0)
diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
index d17ea2d..7fabe0f 100644
--- a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -28,7 +28,7 @@
 import androidx.annotation.RequiresApi
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.window.TestActivity
-import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
+import androidx.window.WindowTestUtils.Companion.assumeAtLeastWindowExtensionVersion
 import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_PRESENT_ON_AREA
 import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_TRANSFER_ACTIVITY_TO_AREA
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
@@ -82,7 +82,7 @@
     fun testRearFacingWindowAreaInfoList(): Unit =
         testScope.runTest {
             assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q)
-            assumeAtLeastVendorApiLevel(minVendorApiLevel)
+            assumeAtLeastWindowExtensionVersion(minVendorApiLevel)
             activityScenario.scenario.onActivity {
                 val extensionComponent = FakeWindowAreaComponent()
                 val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
@@ -163,7 +163,7 @@
     @Test
     fun testTransferToRearFacingWindowArea(): Unit =
         testScope.runTest {
-            assumeAtLeastVendorApiLevel(minVendorApiLevel)
+            assumeAtLeastWindowExtensionVersion(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
             extensions.currentRearDisplayStatus = STATUS_AVAILABLE
@@ -234,7 +234,7 @@
         initialState: @WindowAreaComponent.WindowAreaStatus Int
     ) =
         testScope.runTest {
-            assumeAtLeastVendorApiLevel(minVendorApiLevel)
+            assumeAtLeastWindowExtensionVersion(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
             extensions.currentRearDisplayStatus = initialState
@@ -278,7 +278,7 @@
     @Test
     fun testPresentRearDisplayArea(): Unit =
         testScope.runTest {
-            assumeAtLeastVendorApiLevel(minVendorApiLevel)
+            assumeAtLeastWindowExtensionVersion(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
 
@@ -323,7 +323,7 @@
     @Test
     fun testRearDisplayPresentationModeSessionEndedError(): Unit =
         testScope.runTest {
-            assumeAtLeastVendorApiLevel(minVendorApiLevel)
+            assumeAtLeastWindowExtensionVersion(minVendorApiLevel)
             val extensionComponent = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
 
@@ -361,7 +361,7 @@
     @Test
     fun testPresentContentWithNewControllerThrowsException(): Unit =
         testScope.runTest {
-            assumeAtLeastVendorApiLevel(minVendorApiLevel)
+            assumeAtLeastWindowExtensionVersion(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
 
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
index a44742b..afe4ef6 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
@@ -61,8 +61,8 @@
 
     @Test
     fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel2() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(2)
-        WindowTestUtils.assumeBeforeVendorApiLevel(3)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(2)
+        WindowTestUtils.assumeBeforeWindowExtensionVersion(3)
 
         val oemSplitInfo = createTestOEMSplitInfo(OEMSplitAttributes.Builder().build())
         val expectedSplitInfo =
@@ -80,8 +80,8 @@
     @Suppress("DEPRECATION")
     @Test
     fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel3() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
-        WindowTestUtils.assumeBeforeVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(3)
+        WindowTestUtils.assumeBeforeWindowExtensionVersion(5)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
@@ -103,7 +103,7 @@
 
     @Test
     fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel5() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(5)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
@@ -135,8 +135,8 @@
 
     @Test
     fun testTranslateSplitInfoWithExpandingContainersWithApiLevel2() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(2)
-        WindowTestUtils.assumeBeforeVendorApiLevel(3)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(2)
+        WindowTestUtils.assumeBeforeWindowExtensionVersion(3)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
@@ -159,8 +159,8 @@
     @Suppress("DEPRECATION")
     @Test
     fun testTranslateSplitInfoWithExpandingContainersWithApiLevel3() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
-        WindowTestUtils.assumeBeforeVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(3)
+        WindowTestUtils.assumeBeforeWindowExtensionVersion(5)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
@@ -184,7 +184,7 @@
 
     @Test
     fun testTranslateSplitInfoWithExpandingContainersWithApiLevel5() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(5)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
@@ -219,7 +219,7 @@
     @Suppress("DEPRECATION")
     @Test
     fun testTranslateSplitInfoWithApiLevel1() {
-        WindowTestUtils.assumeBeforeVendorApiLevel(2)
+        WindowTestUtils.assumeBeforeWindowExtensionVersion(2)
 
         val activityStack = createTestOEMActivityStack(ArrayList(), true)
         val expectedSplitRatio = 0.3f
@@ -245,8 +245,8 @@
 
     @Test
     fun testTranslateSplitInfoWithApiLevel2() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(2)
-        WindowTestUtils.assumeBeforeVendorApiLevel(3)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(2)
+        WindowTestUtils.assumeBeforeWindowExtensionVersion(3)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
@@ -270,8 +270,8 @@
     @Suppress("DEPRECATION")
     @Test
     fun testTranslateSplitInfoWithApiLevel3() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
-        WindowTestUtils.assumeBeforeVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(3)
+        WindowTestUtils.assumeBeforeWindowExtensionVersion(5)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
@@ -296,7 +296,7 @@
 
     @Test
     fun testTranslateSplitInfoWithApiLevel5() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(5)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
@@ -331,7 +331,7 @@
 
     @Test
     fun testTranslateAnimationBackgroundWithApiLevel5() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(5)
 
         val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
         val splitAttributesWithColorBackground =
@@ -378,8 +378,8 @@
 
     @Test
     fun testTranslateAnimationBackgroundBeforeApiLevel5() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(2)
-        WindowTestUtils.assumeBeforeVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(2)
+        WindowTestUtils.assumeBeforeWindowExtensionVersion(5)
 
         val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
         val splitAttributesWithColorBackground =
@@ -402,7 +402,7 @@
     @OptIn(androidx.window.core.ExperimentalWindowApi::class)
     @Test
     fun testTranslateEmbeddingConfigurationToWindowAttributes() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(5)
 
         val dimAreaBehavior = EmbeddingConfiguration.DimAreaBehavior.ON_TASK
         adapter.embeddingConfiguration =
@@ -414,7 +414,7 @@
 
     @Test
     fun testTranslateDividerAttributes_draggable() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(6)
         val dividerAttributes =
             DraggableDividerAttributes.Builder()
                 .setWidthDp(20)
@@ -435,7 +435,7 @@
 
     @Test
     fun testTranslateDividerAttributes_fixed() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(6)
         val dividerAttributes =
             FixedDividerAttributes.Builder().setWidthDp(20).setColor(Color.GRAY).build()
         val oemDividerAttributes =
@@ -467,7 +467,7 @@
 
     @Test
     fun testTranslateDividerAttributes_0width_withApiLevel7() {
-        WindowTestUtils.assumeVendorApiLevel(7)
+        WindowTestUtils.assumeWindowExtensionVersionEquals(7)
         val dividerAttributes =
             DraggableDividerAttributes.Builder().setWidthDp(0).setColor(Color.GRAY).build()
 
@@ -484,7 +484,7 @@
 
     @Test
     fun testTranslateDividerAttributes_noDivider() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+        WindowTestUtils.assumeAtLeastWindowExtensionVersion(6)
         val dividerAttributes = DividerAttributes.NO_DIVIDER
         val oemDividerAttributes = null
 
diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
index 7cf977a..30c985d 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
@@ -27,8 +27,8 @@
 import androidx.window.TestActivity
 import androidx.window.WindowSdkExtensions
 import androidx.window.WindowTestUtils
-import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
-import androidx.window.WindowTestUtils.Companion.assumeBeforeVendorApiLevel
+import androidx.window.WindowTestUtils.Companion.assumeAtLeastWindowExtensionVersion
+import androidx.window.WindowTestUtils.Companion.assumeBeforeWindowExtensionVersion
 import androidx.window.layout.adapter.WindowBackend
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.TruthJUnit.assume
@@ -91,7 +91,7 @@
     fun testWindowLayoutInfo_contextAsListener() =
         testScope.runTest {
             assume().that(Build.VERSION.SDK_INT).isAtLeast(Build.VERSION_CODES.R)
-            assumeAtLeastVendorApiLevel(2)
+            assumeAtLeastWindowExtensionVersion(2)
             Dispatchers.setMain(testDispatcher) // Needed for flowOn(Dispatchers.Main).
             val collector = mutableListOf<WindowLayoutInfo>()
             val windowContext = WindowTestUtils.createOverlayWindowContext()
@@ -127,7 +127,7 @@
     fun testWindowLayoutInfo_multicastingWithContext() =
         testScope.runTest {
             assume().that(Build.VERSION.SDK_INT).isAtLeast(Build.VERSION_CODES.R)
-            assumeAtLeastVendorApiLevel(2)
+            assumeAtLeastWindowExtensionVersion(2)
             Dispatchers.setMain(testDispatcher) // Needed for flowOn(Dispatchers.Main).
             val collector = mutableListOf<WindowLayoutInfo>()
             val windowContext = WindowTestUtils.createOverlayWindowContext()
@@ -146,7 +146,7 @@
     fun testWindowLayoutInfo_nonUiContext_throwsError() =
         testScope.runTest {
             assume().that(Build.VERSION.SDK_INT).isAtLeast(Build.VERSION_CODES.R)
-            assumeAtLeastVendorApiLevel(2)
+            assumeAtLeastWindowExtensionVersion(2)
             Dispatchers.setMain(testDispatcher) // Needed for flowOn(Dispatchers.Main).
             val context: Context = ApplicationProvider.getApplicationContext()
             val tracker = WindowInfoTracker.getOrCreate(context)
@@ -160,7 +160,7 @@
 
     @Test
     fun testSupportedWindowPostures_throwsBeforeApi6() {
-        assumeBeforeVendorApiLevel(6)
+        assumeBeforeWindowExtensionVersion(6)
         activityScenario.scenario.onActivity { _ ->
             assertFailsWith<UnsupportedOperationException> { tracker.supportedPostures }
         }
@@ -168,7 +168,7 @@
 
     @Test
     fun testSupportedWindowPostures_reportsFeatures() {
-        assumeAtLeastVendorApiLevel(6)
+        assumeAtLeastWindowExtensionVersion(6)
         activityScenario.scenario.onActivity { _ ->
             val fakeBackend =
                 FakeWindowBackend(supportedPostures = listOf(SupportedPosture.TABLETOP))
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
index 22b63c7..e4f0e55 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
@@ -30,8 +30,8 @@
 import androidx.window.TestActivity
 import androidx.window.TestConsumer
 import androidx.window.WindowTestUtils
-import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
-import androidx.window.WindowTestUtils.Companion.assumeBeforeVendorApiLevel
+import androidx.window.WindowTestUtils.Companion.assumeAtLeastWindowExtensionVersion
+import androidx.window.WindowTestUtils.Companion.assumeBeforeWindowExtensionVersion
 import androidx.window.core.ConsumerAdapter
 import androidx.window.core.ExtensionsUtil
 import androidx.window.extensions.core.util.function.Consumer as OEMConsumer
@@ -81,7 +81,7 @@
 
     @Test
     fun testExtensionWindowBackend_delegatesToWindowLayoutComponent() {
-        assumeAtLeastVendorApiLevel(1)
+        assumeAtLeastWindowExtensionVersion(1)
         val component = RequestTrackingWindowComponent()
 
         val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -99,7 +99,7 @@
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component = RequestTrackingWindowComponent()
 
@@ -123,7 +123,7 @@
     @Suppress("Deprecation")
     @Test
     fun testExtensionWindowBackend_registerAtMostOnce() {
-        assumeBeforeVendorApiLevel(2)
+        assumeBeforeWindowExtensionVersion(2)
         val component = mock<WindowLayoutComponent>()
 
         val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -143,7 +143,7 @@
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component = mock<WindowLayoutComponent>()
 
@@ -203,7 +203,7 @@
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component = FakeWindowComponent()
         val windowContext = WindowTestUtils.createOverlayWindowContext()
@@ -227,7 +227,7 @@
     @Suppress("NewApi", "Deprecation") // java.util.function.Consumer was added in API 24 (N)
     @Test
     fun testExtensionWindowBackend_infoReplayedForAdditionalListener() {
-        assumeBeforeVendorApiLevel(2)
+        assumeBeforeWindowExtensionVersion(2)
         assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
 
         val component =
@@ -257,7 +257,7 @@
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component =
             mock<WindowLayoutComponent> {
@@ -289,7 +289,7 @@
     @Suppress("Deprecation")
     @Test
     fun testExtensionWindowBackend_removeMatchingCallback() {
-        assumeBeforeVendorApiLevel(2)
+        assumeBeforeWindowExtensionVersion(2)
         val component = mock<WindowLayoutComponent>()
 
         val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -309,7 +309,7 @@
     @Suppress("Deprecation")
     @Test
     fun testExtensionWindowBackend_removesMultipleCallback() {
-        assumeBeforeVendorApiLevel(2)
+        assumeBeforeWindowExtensionVersion(2)
         val component = mock<WindowLayoutComponent>()
 
         val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -341,7 +341,7 @@
             // createWindowContext is available after R.
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component = mock<WindowLayoutComponent>()
 
@@ -377,7 +377,7 @@
             // createWindowContext is available after R.
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component = mock<WindowLayoutComponent>()
 
@@ -421,7 +421,7 @@
     @Suppress("Deprecation")
     @Test
     fun testExtensionWindowBackend_reRegisterCallback() {
-        assumeBeforeVendorApiLevel(2)
+        assumeBeforeWindowExtensionVersion(2)
         val component = mock<WindowLayoutComponent>()
 
         val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -448,7 +448,7 @@
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component = mock<WindowLayoutComponent>()
 
@@ -480,7 +480,7 @@
 
     @Test
     fun testRegisterLayoutChangeCallback_clearListeners() {
-        assumeBeforeVendorApiLevel(2)
+        assumeBeforeWindowExtensionVersion(2)
         activityScenario.scenario.onActivity { activity ->
             val component = FakeWindowComponent()
             val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -518,7 +518,7 @@
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         activityScenario.scenario.onActivity { activity ->
             val component = FakeWindowComponent()
@@ -550,7 +550,7 @@
     @RequiresApi(Build.VERSION_CODES.R)
     @Test
     fun testLayoutChangeCallback_emitNewValue() {
-        assumeBeforeVendorApiLevel(2)
+        assumeBeforeWindowExtensionVersion(2)
         activityScenario.scenario.onActivity { activity ->
             val component = FakeWindowComponent()
             val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -571,7 +571,7 @@
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component = FakeWindowComponent()
         val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -589,7 +589,7 @@
     @RequiresApi(Build.VERSION_CODES.R)
     @Test
     fun testWindowLayoutInfo_updatesOnSubsequentRegistration() {
-        assumeAtLeastVendorApiLevel(1)
+        assumeAtLeastWindowExtensionVersion(1)
         activityScenario.scenario.onActivity { activity ->
             val component = FakeWindowComponent()
             val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -616,7 +616,7 @@
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             return
         }
-        assumeAtLeastVendorApiLevel(2)
+        assumeAtLeastWindowExtensionVersion(2)
 
         val component = FakeWindowComponent()
         val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -643,7 +643,7 @@
 
     @Test
     fun testSupportedFeatures_throwsBeforeApi6() {
-        assumeBeforeVendorApiLevel(6)
+        assumeBeforeWindowExtensionVersion(6)
 
         val component = FakeWindowComponent()
         val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
@@ -653,7 +653,7 @@
 
     @Test
     fun testSupportedFeatures_emptyListReturnsNoFeatures() {
-        assumeAtLeastVendorApiLevel(6)
+        assumeAtLeastWindowExtensionVersion(6)
 
         val supportedWindowFeatures = SupportedWindowFeatures.Builder(listOf()).build()
         val component = FakeWindowComponent(windowFeatures = supportedWindowFeatures)
@@ -665,7 +665,7 @@
 
     @Test
     fun testSupportedFeatures_halfOpenedReturnsTabletopSupport() {
-        assumeAtLeastVendorApiLevel(6)
+        assumeAtLeastWindowExtensionVersion(6)
 
         val foldFeature =
             DisplayFoldFeature.Builder(DisplayFoldFeature.TYPE_SCREEN_FOLD_IN)
diff --git a/work/work-runtime/src/main/java/androidx/work/ListenableWorker.java b/work/work-runtime/src/main/java/androidx/work/ListenableWorker.java
index 3d5d37b..2fd3013 100644
--- a/work/work-runtime/src/main/java/androidx/work/ListenableWorker.java
+++ b/work/work-runtime/src/main/java/androidx/work/ListenableWorker.java
@@ -394,8 +394,8 @@
          * Returns an instance of {@link Result} that can be used to indicate that the work
          * completed with a permanent failure. Any work that depends on this will also be marked as
          * failed and will not be run. <b>If you need child workers to run, you need to use
-         * {@link #success()} or {@link #success(Data)}</b>; failure indicates a permanent stoppage
-         * of the chain of work.
+         * {@link #success()} or {@link #success(Data) success(Data)}</b>; failure indicates a
+         * permanent stoppage of the chain of work.
          *
          * @return An instance of {@link Result} indicating failure when executing work
          */
@@ -407,8 +407,8 @@
          * Returns an instance of {@link Result} that can be used to indicate that the work
          * completed with a permanent failure. Any work that depends on this will also be marked as
          * failed and will not be run. <b>If you need child workers to run, you need to use
-         * {@link #success()} or {@link #success(Data)}</b>; failure indicates a permanent stoppage
-         * of the chain of work.
+         * {@link #success()} or {@link #success(Data) success(Data)}</b>; failure indicates a
+         * permanent stoppage of the chain of work.
          *
          * @param outputData A {@link Data} object that can be used to keep track of why the work
          *                   failed
diff --git a/xr/arcore/integration-tests/whitebox/build.gradle b/xr/arcore/integration-tests/whitebox/build.gradle
new file mode 100644
index 0000000..69337e6
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/build.gradle
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.ApkCopyHelperKt
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("AndroidXComposePlugin")
+    id("com.android.application")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    defaultConfig {
+        // TODO: This should be lower, possibly 21.
+        //       Address API calls that require higher versions.
+        minSdkVersion 30
+    }
+    buildFeatures { viewBinding = true }
+    namespace = "androidx.xr.arcore.apps.whitebox"
+}
+
+androidx {
+    name = "ARCore Whitebox"
+    type = LibraryType.TEST_APPLICATION
+    inceptionYear = "2024"
+    description = "Test app that exercises the ARCore APIs."
+    kotlinTarget = KotlinTarget.KOTLIN_1_9
+    // TODO: b/326456246
+    optOutJSpecify = true
+}
+
+dependencies {
+    implementation(project(":xr:arcore:arcore"))
+    implementation(project(":xr:assets"))
+    implementation(project(":xr:compose:compose"))
+    implementation(project(":xr:scenecore:scenecore"))
+
+    implementation(libs.kotlinStdlib)
+    implementation(libs.kotlinCoroutinesGuava)
+    implementation("androidx.activity:activity-compose:1.9.3")
+    implementation("androidx.appcompat:appcompat:1.7.0")
+    implementation("androidx.compose.foundation:foundation-layout:1.7.5")
+    implementation("androidx.compose.material3:material3:1.3.1")
+    implementation("androidx.compose.runtime:runtime:1.7.5")
+    implementation("androidx.compose.ui:ui:1.7.5")
+    implementation("androidx.lifecycle:lifecycle-runtime:2.8.7")
+}
+
+// Making this APK available via Android Build so the QA team can
+// access it in a convenient manner.
+ApkCopyHelperKt.setupAppApkCopy(project, "release")
diff --git a/xr/arcore/integration-tests/whitebox/src/main/AndroidManifest.xml b/xr/arcore/integration-tests/whitebox/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..706b854
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+  <uses-permission android:name="android.permission.SCENE_UNDERSTANDING" />
+  <application android:label="ARCore Whitebox" android:taskAffinity="">
+    <activity
+        android:name=".MainActivity"
+        android:exported="true"
+        android:theme="@style/Theme.AppCompat.NoActionBar">
+      <property
+          android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
+          android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED" />
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+    </activity>
+    <activity
+        android:name=".helloar.HelloArActivity"
+        android:exported="true"
+        android:theme="@style/Theme.AppCompat.NoActionBar">
+    </activity>
+     <activity
+        android:name=".persistentanchors.PersistentAnchorsActivity"
+        android:exported="true"
+        android:theme="@style/Theme.AppCompat.NoActionBar">
+    </activity>
+  </application>
+</manifest>
diff --git a/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/MainActivity.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/MainActivity.kt
new file mode 100644
index 0000000..5a4dc72
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/MainActivity.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.xr.arcore.apps.whitebox
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.xr.arcore.apps.whitebox.helloar.HelloArActivity as HelloArActivity
+import androidx.xr.arcore.apps.whitebox.persistentanchors.PersistentAnchorsActivity as PersistentAnchorsActivity
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+/** Entrypoint for testing various ARCore for Android XR functionalities. */
+class MainActivity : ComponentActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContent { WhiteboxHomeScreen() }
+    }
+}
+
+@Composable
+fun WhiteboxHomeScreen(modifier: Modifier = Modifier) {
+    Surface(modifier = modifier.fillMaxSize(), color = Color.White) {
+        Column() {
+            Text(
+                "AR Whitebox Test Application",
+                modifier = Modifier.padding(20.dp),
+                fontSize = 30.sp,
+                color = Color.Black,
+            )
+            VersionInfoCard()
+            WhiteboxSessionMenu()
+        }
+    }
+}
+
+@Composable
+fun VersionInfoCard() {
+    Card {
+        Column(modifier = Modifier.padding(16.dp)) {
+            Text("Build Fingerprint: ${Build.FINGERPRINT}")
+            Text(
+                "Date: ${SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale.ENGLISH).format(Build.TIME)}"
+            )
+            Text("CL Number: N/A")
+        }
+    }
+}
+
+@Composable
+fun WhiteboxSessionMenu() {
+    val context = LocalContext.current
+
+    Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
+        Text(
+            "Test Activity List",
+            modifier = Modifier.padding(20.dp),
+            fontSize = 20.sp,
+            fontWeight = FontWeight.Bold,
+            color = Color.Black,
+        )
+        HorizontalDivider()
+        TextButton(
+            onClick = { context.startActivity(Intent(context, HelloArActivity::class.java)) }
+        ) {
+            Text("Hello AR")
+        }
+        TextButton(
+            onClick = {
+                context.startActivity(Intent(context, PersistentAnchorsActivity::class.java))
+            }
+        ) {
+            Text("Persistent Anchors")
+        }
+    }
+}
diff --git a/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/common/Composables.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/common/Composables.kt
new file mode 100644
index 0000000..4377346
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/common/Composables.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.xr.arcore.apps.whitebox.common
+
+import android.app.Activity
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.xr.arcore.Plane
+import androidx.xr.arcore.Trackable
+
+@Composable
+fun BackToMainActivityButton() {
+    val context = LocalContext.current
+    FilledTonalButton(onClick = { (context as Activity).finish() }) { Text("Go Back") }
+}
+
+@Composable
+fun TrackablesList(trackables: List<Trackable<Trackable.State>>) {
+    LazyColumn { items(trackables) { trackable -> TrackableCard(trackable) } }
+}
+
+@Composable
+fun TrackableCard(trackable: Trackable<Trackable.State>) {
+    val state = trackable.state.collectAsStateWithLifecycle()
+    OutlinedCard(
+        colors = CardDefaults.cardColors(),
+        modifier = Modifier.padding(8.dp).fillMaxWidth(),
+    ) {
+        Column(modifier = Modifier.padding(16.dp)) {
+            Text(text = "Trackable ID: ${trackable}")
+            Text(text = "Tracking State: ${state.value.trackingState}")
+            if (trackable is Plane) {
+                Text("Plane Type: ${trackable.type}")
+                PlaneStateInfo(state.value as Plane.State)
+            }
+        }
+    }
+}
+
+@Composable
+fun PlaneStateInfo(state: Plane.State) {
+    Text(
+        text = "Plane Label: ${state.label}",
+        color = convertPlaneLabelToColor(state.label),
+    )
+    Text(text = "Plane Center Pose: ${state.centerPose}")
+    Text(text = "Plane Extents: ${state.extents}")
+    Text(text = "Subsumed by Plane: ${state.subsumedBy}")
+    Text(text = "Plane Vertices: ${state.vertices}")
+}
+
+private fun convertPlaneLabelToColor(label: Plane.Label): Color =
+    when (label) {
+        Plane.Label.Wall -> Color.Green
+        Plane.Label.Floor -> Color.Blue
+        Plane.Label.Ceiling -> Color.Yellow
+        Plane.Label.Table -> Color.Magenta
+        else -> Color.Red
+    }
diff --git a/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/common/SessionLifecycleHelper.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/common/SessionLifecycleHelper.kt
new file mode 100644
index 0000000..87d8402
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/common/SessionLifecycleHelper.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.xr.arcore.apps.whitebox.common
+
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
+import androidx.activity.result.registerForActivityResult
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.SessionCreatePermissionsNotGranted
+import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.SessionResumePermissionsNotGranted
+import androidx.xr.runtime.SessionResumeSuccess
+
+/**
+ * Observer class to manage the lifecycle of the Jetpack XR Runtime Session based on the lifecycle
+ * owner (activity).
+ */
+class SessionLifecycleHelper(
+    internal val onCreateCallback: (Session) -> Unit,
+    internal val onResumeCallback: (() -> Unit)? = null,
+    internal val beforePauseCallback: (() -> Unit)? = null,
+) : DefaultLifecycleObserver {
+
+    internal lateinit var session: Session
+    internal lateinit var requestPermissionLauncher: ActivityResultLauncher<Array<String>>
+
+    override fun onCreate(owner: LifecycleOwner) {
+        // Sessions can only be instantiated with an instance of [ComponentActivity].
+        check(owner is ComponentActivity) { "owner is not an instance of ComponentActivity" }
+
+        registerRequestPermissionLauncher(owner)
+
+        when (val result = Session.create(owner)) {
+            is SessionCreateSuccess -> {
+                session = result.session
+                onCreateCallback.invoke(session)
+            }
+            is SessionCreatePermissionsNotGranted -> {
+                requestPermissionLauncher.launch(result.permissions.toTypedArray())
+            }
+        }
+    }
+
+    override fun onResume(owner: LifecycleOwner) {
+        if (!this::session.isInitialized) {
+            return
+        }
+        when (val result = session.resume()) {
+            is SessionResumeSuccess -> {
+                onResumeCallback?.invoke()
+            }
+            is SessionResumePermissionsNotGranted -> {
+                requestPermissionLauncher.launch(result.permissions.toTypedArray())
+            }
+            else -> {
+                showErrorMessage("Attempted to resume while session is null.")
+            }
+        }
+    }
+
+    override fun onPause(owner: LifecycleOwner) {
+        if (!this::session.isInitialized) {
+            return
+        }
+        beforePauseCallback?.invoke()
+        session.pause()
+    }
+
+    override fun onDestroy(owner: LifecycleOwner) {
+        if (!this::session.isInitialized) {
+            return
+        }
+        session.destroy()
+    }
+
+    private fun registerRequestPermissionLauncher(activity: ComponentActivity) {
+        requestPermissionLauncher =
+            activity.registerForActivityResult(
+                ActivityResultContracts.RequestMultiplePermissions()
+            ) { permissions ->
+                val allPermissionsGranted = permissions.all { it.value }
+                if (!allPermissionsGranted) {
+                    Toast.makeText(
+                            activity,
+                            "Required permissions were not granted, closing activity. ",
+                            Toast.LENGTH_LONG,
+                        )
+                        .show()
+                    activity.finish()
+                } else {
+                    activity.recreate()
+                }
+            }
+    }
+
+    private fun <F> showErrorMessage(error: F) {
+        Log.e(TAG, error.toString())
+    }
+
+    companion object {
+        private val TAG = this::class.simpleName
+    }
+}
diff --git a/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/HelloArActivity.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/HelloArActivity.kt
new file mode 100644
index 0000000..d8683a5
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/HelloArActivity.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.xr.arcore.apps.whitebox.helloar
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.xr.arcore.apps.whitebox.common.BackToMainActivityButton
+import androidx.xr.arcore.apps.whitebox.common.SessionLifecycleHelper
+import androidx.xr.arcore.apps.whitebox.common.TrackablesList
+import androidx.xr.arcore.apps.whitebox.helloar.rendering.AnchorRenderer
+import androidx.xr.arcore.apps.whitebox.helloar.rendering.PlaneRenderer
+import androidx.xr.arcore.perceptionState
+import androidx.xr.runtime.Session
+import androidx.xr.scenecore.Session as JxrCoreSession
+
+/** Sample that demonstrates fundamental ARCore for Android XR usage. */
+class HelloArActivity : ComponentActivity() {
+
+    private lateinit var session: Session
+    private lateinit var sessionHelper: SessionLifecycleHelper
+
+    private lateinit var jxrCoreSession: JxrCoreSession
+
+    private var planeRenderer: PlaneRenderer? = null
+    private var anchorRenderer: AnchorRenderer? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        // Create session and renderers.
+        sessionHelper =
+            SessionLifecycleHelper(
+                onCreateCallback = {
+                    session = it
+                    jxrCoreSession = JxrCoreSession.create(this)
+                    planeRenderer = PlaneRenderer(session, jxrCoreSession, lifecycleScope)
+                    anchorRenderer =
+                        AnchorRenderer(
+                            this,
+                            planeRenderer!!,
+                            session,
+                            jxrCoreSession,
+                            lifecycleScope
+                        )
+                    setContent { HelloWorld(session) }
+                },
+                onResumeCallback = {
+                    planeRenderer?.startRendering()
+                    anchorRenderer?.startRendering()
+                },
+                beforePauseCallback = {
+                    planeRenderer?.stopRendering()
+                    anchorRenderer?.stopRendering()
+                },
+            )
+        lifecycle.addObserver(sessionHelper)
+    }
+}
+
+@Composable
+fun HelloWorld(session: Session) {
+    val state by session.state.collectAsStateWithLifecycle()
+    val perceptionState = state.perceptionState
+
+    Column(modifier = Modifier.background(color = Color.White)) {
+        BackToMainActivityButton()
+        Text(text = "CoreState: ${state.timeMark}")
+        if (perceptionState != null) {
+            TrackablesList(perceptionState.trackables.toList())
+        } else {
+            Text("PerceptionState is null.")
+        }
+    }
+}
diff --git a/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.android.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/AnchorModel.kt
similarity index 60%
rename from compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.android.kt
rename to xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/AnchorModel.kt
index c487bdc..a721aff 100644
--- a/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/collection/ArrayUtils.android.kt
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/AnchorModel.kt
@@ -14,16 +14,17 @@
  * limitations under the License.
  */
 
-@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+package androidx.xr.arcore.apps.whitebox.helloar.rendering
 
-package androidx.compose.runtime.collection
+import androidx.xr.arcore.Anchor
+import androidx.xr.scenecore.Entity
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.StateFlow
 
-internal actual inline fun <T> Array<out T>.fastCopyInto(
-    destination: Array<T>,
-    destinationOffset: Int,
-    startIndex: Int,
-    endIndex: Int
-): Array<T> {
-    System.arraycopy(this, startIndex, destination, destinationOffset, endIndex - startIndex)
-    return destination
-}
+/** Represents a rendered anchor model. */
+data class AnchorModel(
+    val id: Int,
+    val stateFlow: StateFlow<Anchor.State>,
+    internal val entity: Entity,
+    internal val renderJob: Job?,
+) {}
diff --git a/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/AnchorRenderer.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/AnchorRenderer.kt
new file mode 100644
index 0000000..a56dacb
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/AnchorRenderer.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.xr.arcore.apps.whitebox.helloar.rendering
+
+import android.app.Activity
+import android.util.Log
+import androidx.xr.arcore.Anchor
+import androidx.xr.arcore.AnchorCreateResourcesExhausted
+import androidx.xr.arcore.AnchorCreateSuccess
+import androidx.xr.arcore.Plane
+import androidx.xr.arcore.TrackingState
+import androidx.xr.arcore.hitTest
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Ray
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.GltfModel
+import androidx.xr.scenecore.InputEvent
+import androidx.xr.scenecore.InteractableComponent
+import androidx.xr.scenecore.Session as JxrCoreSession
+import kotlinx.coroutines.CompletableJob
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.guava.await
+import kotlinx.coroutines.launch
+
+/** Class that keeps track of anchors rendered as GLTF models in a SceneCore session. */
+internal class AnchorRenderer(
+    val activity: Activity,
+    val planeRenderer: PlaneRenderer,
+    val session: Session,
+    val renderSession: JxrCoreSession,
+    val coroutineScope: CoroutineScope,
+) {
+
+    private lateinit var gltfAnchorModel: GltfModel
+
+    private val renderedAnchors: MutableList<AnchorModel> = mutableListOf<AnchorModel>()
+
+    private lateinit var updateJob: CompletableJob
+
+    internal fun startRendering() {
+        updateJob =
+            SupervisorJob(
+                coroutineScope.launch() {
+                    gltfAnchorModel =
+                        renderSession.createGltfResourceAsync("models/xyzArrows.glb").await()
+                    planeRenderer.renderedPlanes.collect { attachInteractableComponents(it) }
+                }
+            )
+    }
+
+    internal fun stopRendering() {
+        updateJob.complete()
+        clearRenderedAnchors()
+    }
+
+    private fun clearRenderedAnchors() {
+        for (anchor in renderedAnchors) {
+            anchor.entity.dispose()
+        }
+        renderedAnchors.clear()
+    }
+
+    private fun attachInteractableComponents(planeModels: Collection<PlaneModel>) {
+        for (planeModel in planeModels) {
+            if (planeModel.entity.getComponents().isEmpty()) {
+                planeModel.entity.addComponent(
+                    InteractableComponent.create(renderSession, activity.mainExecutor) { event ->
+                        if (event.action.equals(InputEvent.ACTION_DOWN)) {
+                            val up =
+                                renderSession.spatialUser.head?.getActivitySpacePose()?.up
+                                    ?: Vector3.Up
+                            val perceptionRayPose =
+                                renderSession.activitySpace.transformPoseTo(
+                                    Pose(
+                                        event.origin,
+                                        Quaternion.fromLookTowards(event.direction, up)
+                                    ),
+                                    renderSession.perceptionSpace,
+                                )
+                            val perceptionRay =
+                                Ray(perceptionRayPose.translation, perceptionRayPose.forward)
+                            hitTest(session, perceptionRay)
+                                .firstOrNull {
+                                    // TODO(b/372054517): Re-enable creating anchors on Unknown
+                                    // planes once we can
+                                    // support rendering them.
+                                    (it.trackable as? Plane)?.state?.value?.label !=
+                                        Plane.Label.Unknown
+                                }
+                                ?.let { hitResult ->
+                                    try {
+                                        when (
+                                            val anchorResult =
+                                                Anchor.create(session, hitResult.hitPose)
+                                        ) {
+                                            is AnchorCreateSuccess ->
+                                                renderedAnchors.add(
+                                                    createAnchorModel(anchorResult.anchor)
+                                                )
+                                            is AnchorCreateResourcesExhausted -> {}
+                                        }
+                                    } catch (e: IllegalStateException) {
+                                        Log.e(
+                                            activity::class.simpleName,
+                                            "Failed to create anchor: ${e.message}"
+                                        )
+                                    }
+                                }
+                        }
+                    }
+                )
+            }
+        }
+    }
+
+    private fun createAnchorModel(anchor: Anchor): AnchorModel {
+        val entity = renderSession.createGltfEntity(gltfAnchorModel, Pose())
+        entity.setScale(.1f)
+        val renderJob =
+            coroutineScope.launch(updateJob) {
+                anchor.state.collect { state ->
+                    if (state.trackingState == TrackingState.Tracking) {
+                        entity.setPose(
+                            renderSession.perceptionSpace.transformPoseTo(
+                                state.pose,
+                                renderSession.activitySpace
+                            )
+                        )
+                    } else if (state.trackingState == TrackingState.Stopped) {
+                        entity.setHidden(true)
+                    }
+                }
+            }
+        return AnchorModel(anchor.hashCode(), anchor.state, entity, renderJob)
+    }
+}
diff --git a/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/PlaneModel.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/PlaneModel.kt
new file mode 100644
index 0000000..7fb4fd8
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/PlaneModel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.xr.arcore.apps.whitebox.helloar.rendering
+
+import androidx.xr.arcore.Plane
+import androidx.xr.scenecore.Entity
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.StateFlow
+
+/** Represents a rendered plane model. */
+data class PlaneModel(
+    val id: Int,
+    val type: Plane.Type,
+    val stateFlow: StateFlow<Plane.State>,
+    internal val entity: Entity,
+    internal val renderJob: Job?,
+) {}
diff --git a/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/PlaneRenderer.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/PlaneRenderer.kt
new file mode 100644
index 0000000..b60ab49
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/helloar/rendering/PlaneRenderer.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.xr.arcore.apps.whitebox.helloar.rendering
+
+import android.app.Activity
+import android.content.res.Resources
+import android.graphics.Color
+import android.view.View
+import android.widget.TextView
+import androidx.xr.arcore.Plane
+import androidx.xr.arcore.TrackingState
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector2
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.Dimensions
+import androidx.xr.scenecore.PanelEntity
+import androidx.xr.scenecore.PixelDimensions
+import androidx.xr.scenecore.Session as JxrCoreSession
+import kotlinx.coroutines.CompletableJob
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+/** Class that keeps track of planes rendered as GLTF models in a SceneCore session. */
+internal class PlaneRenderer(
+    val session: Session,
+    val renderSession: JxrCoreSession,
+    val coroutineScope: CoroutineScope,
+) {
+
+    private val _renderedPlanes: MutableStateFlow<List<PlaneModel>> =
+        MutableStateFlow(mutableListOf<PlaneModel>())
+    internal val renderedPlanes: StateFlow<Collection<PlaneModel>> = _renderedPlanes.asStateFlow()
+
+    private lateinit var updateJob: CompletableJob
+
+    internal fun startRendering() {
+        updateJob =
+            SupervisorJob(
+                coroutineScope.launch { Plane.subscribe(session).collect { updatePlaneModels(it) } }
+            )
+    }
+
+    internal fun stopRendering() {
+        updateJob.complete()
+        _renderedPlanes.value = emptyList<PlaneModel>()
+    }
+
+    private fun updatePlaneModels(planes: Collection<Plane>) {
+        val planesToRender = _renderedPlanes.value.toMutableList()
+        // Create renderers for new planes.
+        for (plane in planes) {
+            if (_renderedPlanes.value.none { it.id == plane.hashCode() }) {
+                addPlaneModel(plane, planesToRender)
+            }
+        }
+        // Stop rendering dropped planes.
+        for (renderedPlane in _renderedPlanes.value) {
+            if (planes.none { it.hashCode() == renderedPlane.id }) {
+                removePlaneModel(renderedPlane, planesToRender)
+            }
+        }
+        // Emit to notify collectors that collection has been updated.
+        _renderedPlanes.value = planesToRender
+    }
+
+    private fun addPlaneModel(plane: Plane, planesToRender: MutableList<PlaneModel>) {
+        val view = createPanelDebugViewUsingCompose(plane, renderSession.activity)
+        val entity = createPlanePanelEntity(plane, view)
+        // The counter starts at max to trigger the resize on the first update loop since emulators
+        // only
+        // update their static planes once.
+        var counter = PANEL_RESIZE_UPDATE_COUNT
+        // Make the render job a child of the update job so it completes when the parent completes.
+        val renderJob =
+            coroutineScope.launch(updateJob) {
+                plane.state.collect { state ->
+                    if (state.trackingState == TrackingState.Tracking) {
+                        if (state.label == Plane.Label.Unknown) {
+                            entity.setHidden(true)
+                        } else {
+                            entity.setHidden(false)
+                            counter++
+                            entity.setPose(
+                                renderSession.perceptionSpace
+                                    .transformPoseTo(state.centerPose, renderSession.activitySpace)
+                                    // Planes are X-Y while Panels are X-Z, so we need to rotate the
+                                    // X-axis by -90
+                                    // degrees to align them.
+                                    .compose(PANEL_TO_PLANE_ROTATION)
+                            )
+
+                            updateViewText(view, plane, state)
+                            if (counter > PANEL_RESIZE_UPDATE_COUNT) {
+                                val panelExtentsInPixels = convertMetersToPixels(state.extents)
+                                entity.setPixelDimensions(
+                                    PixelDimensions(
+                                        width = panelExtentsInPixels.x.toInt(),
+                                        height = panelExtentsInPixels.y.toInt(),
+                                    )
+                                )
+                                counter = 0
+                            }
+                        }
+                    } else if (state.trackingState == TrackingState.Stopped) {
+                        entity.setHidden(true)
+                    }
+                }
+            }
+
+        planesToRender.add(PlaneModel(plane.hashCode(), plane.type, plane.state, entity, renderJob))
+    }
+
+    private fun createPlanePanelEntity(plane: Plane, view: View): PanelEntity {
+        return renderSession.createPanelEntity(
+            view,
+            Dimensions(320f, 320f),
+            Dimensions(1f, 1f, 1f),
+            plane.hashCode().toString(),
+            plane.state.value.centerPose,
+        )
+    }
+
+    private fun createPanelDebugViewUsingCompose(plane: Plane, activity: Activity): View {
+        val view = TextView(activity.applicationContext)
+        view.text = "Plane: ${plane.hashCode()}"
+        view.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM)
+        view.setBackgroundColor(Color.WHITE)
+        return view
+    }
+
+    private fun updateViewText(view: View, plane: Plane, state: Plane.State) {
+        val textView = view as TextView
+        textView.setBackgroundColor(convertPlaneLabelToColor(state.label))
+        textView.text = "Plane: ${plane.hashCode()}"
+    }
+
+    private fun convertPlaneLabelToColor(label: Plane.Label): Int =
+        when (label) {
+            Plane.Label.Wall -> Color.GREEN
+            Plane.Label.Floor -> Color.BLUE
+            Plane.Label.Ceiling -> Color.YELLOW
+            Plane.Label.Table -> Color.MAGENTA
+            // Planes with Unknown Label are currently not rendered.
+            else -> Color.RED
+        }
+
+    private fun convertMetersToPixels(input: Vector2): Vector2 = input * PX_PER_METER
+
+    private fun removePlaneModel(planeModel: PlaneModel, planesToRender: MutableList<PlaneModel>) {
+        planeModel.renderJob?.cancel()
+        planeModel.entity.dispose()
+        planesToRender.remove(planeModel)
+    }
+
+    private companion object {
+        private val PX_PER_METER = Resources.getSystem().displayMetrics.density * 1111.11f
+        private val PANEL_TO_PLANE_ROTATION =
+            Pose(Vector3(), Quaternion.fromEulerAngles(-90f, 0f, 0f))
+        private const val PANEL_RESIZE_UPDATE_COUNT = 50
+    }
+}
diff --git a/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/persistentanchors/PersistentAnchorsActivity.kt b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/persistentanchors/PersistentAnchorsActivity.kt
new file mode 100644
index 0000000..58a806f
--- /dev/null
+++ b/xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/persistentanchors/PersistentAnchorsActivity.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.xr.arcore.apps.whitebox.persistentanchors
+
+import android.app.Activity
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
+import androidx.savedstate.SavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import androidx.xr.arcore.Anchor
+import androidx.xr.arcore.AnchorCreateSuccess
+import androidx.xr.arcore.apps.whitebox.common.BackToMainActivityButton
+import androidx.xr.arcore.apps.whitebox.common.SessionLifecycleHelper
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.Dimensions
+import androidx.xr.scenecore.Entity
+import androidx.xr.scenecore.Session as JxrCoreSession
+import java.util.UUID
+import kotlin.collections.List
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+/** Activity to test the Persistent Anchor APIs. */
+class PersistentAnchorsActivity : ComponentActivity() {
+
+    private lateinit var session: Session
+    private lateinit var sessionHelper: SessionLifecycleHelper
+    private lateinit var jxrCoreSession: JxrCoreSession
+    private lateinit var movableEntity: Entity
+    private val movableEntityOffset = Pose(Vector3(0f, 0f, -2.0f))
+    private val uuids = MutableStateFlow<List<UUID>>(emptyList())
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        sessionHelper =
+            SessionLifecycleHelper(
+                onCreateCallback = {
+                    session = it
+                    jxrCoreSession = JxrCoreSession.create(this)
+                    createTargetPanel()
+                    setContent { MainPanel() }
+                },
+                onResumeCallback = { onResumeCallback() },
+            )
+        lifecycle.addObserver(sessionHelper)
+    }
+
+    private fun createTargetPanel() {
+        val composeView = ComposeView(this)
+        composeView.setContent { TargetPanel() }
+        configureComposeView(composeView, this)
+        movableEntity =
+            jxrCoreSession.createPanelEntity(
+                composeView,
+                Dimensions(640f, 640f),
+                Dimensions(1f, 1f, 1f),
+                "movableEntity",
+                movableEntityOffset,
+            )
+        movableEntity.setParent(jxrCoreSession.activitySpace)
+    }
+
+    private fun onResumeCallback() {
+        lifecycleScope.launch {
+            // First load will fail, so we launch a second load after a delay which should succeed.
+            uuids.emit(Anchor.getPersistedAnchorUuids(session))
+            delay(2.seconds)
+            uuids.emit(Anchor.getPersistedAnchorUuids(session))
+        }
+        lifecycleScope.launch { session.state.collect { updatePlaneEntity() } }
+    }
+
+    private fun updatePlaneEntity() {
+        jxrCoreSession.spatialUser.head?.let {
+            movableEntity.setPose(
+                it.transformPoseTo(movableEntityOffset, jxrCoreSession.activitySpace)
+            )
+        }
+    }
+
+    private fun configureComposeView(composeView: ComposeView, activity: Activity) {
+        composeView.setViewCompositionStrategy(
+            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+        )
+        composeView.setViewTreeLifecycleOwner(activity as LifecycleOwner)
+        composeView.setViewTreeViewModelStoreOwner(activity as ViewModelStoreOwner)
+        composeView.setViewTreeSavedStateRegistryOwner(activity as SavedStateRegistryOwner)
+    }
+
+    @Composable
+    private fun MainPanel() {
+        val uuidsState = uuids.collectAsStateWithLifecycle()
+
+        Column(
+            modifier =
+                Modifier.background(color = Color.White)
+                    .fillMaxHeight()
+                    .fillMaxWidth()
+                    .padding(horizontal = 20.dp)
+        ) {
+            BackToMainActivityButton()
+            Text(modifier = Modifier.padding(top = 20.dp), text = "Persisted anchors:")
+            for (uuid in uuidsState.value) {
+                Row(modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp)) {
+                    Text(text = "UUID: $uuid", fontSize = 24.sp)
+                    Button(onClick = { loadAnchor(uuid) }) { Text("Load anchor") }
+                    Button(onClick = { unpersistAnchor(uuid) }) { Text("Unpersist anchor") }
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun TargetPanel() {
+        Column(
+            modifier =
+                Modifier.background(color = Color.White)
+                    .fillMaxHeight()
+                    .fillMaxWidth()
+                    .padding(horizontal = 20.dp),
+            verticalArrangement = Arrangement.Center,
+        ) {
+            Button(onClick = { addAnchor() }) { Text(text = "Add anchor", fontSize = 38.sp) }
+        }
+    }
+
+    private fun addAnchor() {
+        val anchorPose =
+            jxrCoreSession.activitySpace.transformPoseTo(
+                movableEntity.getPose(),
+                jxrCoreSession.perceptionSpace,
+            )
+        val anchor = (Anchor.create(session, anchorPose) as AnchorCreateSuccess).anchor
+        createAnchorPanel(anchor)
+    }
+
+    private fun createAnchorPanel(anchor: Anchor) {
+        val composeView = ComposeView(this)
+        configureComposeView(composeView, this)
+        val anchorEntity = jxrCoreSession.createAnchorEntity(anchor)
+        val panelEntity =
+            jxrCoreSession.createPanelEntity(
+                composeView,
+                Dimensions(640f, 640f),
+                Dimensions(1f, 1f, 1f),
+                "anchorEntity ${anchor.hashCode()}",
+                Pose(),
+            )
+        panelEntity.setParent(anchorEntity)
+        composeView.setContent { AnchorPanel(anchor, panelEntity) }
+    }
+
+    @Composable
+    private fun AnchorPanel(anchor: Anchor, entity: Entity) {
+        val anchorState = anchor.state.collectAsStateWithLifecycle()
+        Column(
+            modifier =
+                Modifier.background(color = Color.White)
+                    .fillMaxHeight()
+                    .fillMaxWidth()
+                    .padding(horizontal = 20.dp),
+            verticalArrangement = Arrangement.Center,
+        ) {
+            Text(
+                modifier = Modifier.padding(top = 10.dp),
+                text = "Tracking State: ${anchorState.value.trackingState}",
+                fontSize = 32.sp,
+            )
+            Button(modifier = Modifier.padding(top = 10.dp), onClick = { persistAnchor(anchor) }) {
+                Text(text = "Persist anchor", fontSize = 32.sp)
+            }
+            Button(
+                modifier = Modifier.padding(top = 10.dp),
+                onClick = { deleteEntity(anchor, entity) }
+            ) {
+                Text(text = "Delete anchor", fontSize = 32.sp)
+            }
+        }
+    }
+
+    private fun persistAnchor(anchor: Anchor) {
+        lifecycleScope.launch {
+            try {
+                anchor.persist()
+                uuids.emit(Anchor.getPersistedAnchorUuids(session))
+            } catch (e: RuntimeException) {
+                Log.e("ARCore", "Error persisting anchor: ${e.message}")
+            }
+        }
+    }
+
+    private fun deleteEntity(anchor: Anchor, entity: Entity) {
+        entity.dispose()
+        anchor.detach()
+    }
+
+    private fun unpersistAnchor(uuid: UUID) {
+        Anchor.unpersist(session, uuid)
+        lifecycleScope.launch { uuids.emit(uuids.value - uuid) }
+    }
+
+    private fun loadAnchor(uuid: UUID) {
+        val anchor = (Anchor.load(session, uuid) as AnchorCreateSuccess).anchor
+        lifecycleScope.launch {
+            // We need to wait until the anchor is tracked before querying its pose.
+            delay(1.seconds)
+            createAnchorPanel(anchor)
+        }
+    }
+}
diff --git a/xr/assets/README.md b/xr/assets/README.md
new file mode 100644
index 0000000..01e95eb
--- /dev/null
+++ b/xr/assets/README.md
@@ -0,0 +1,8 @@
+# Jetpack XR Assets
+
+This library contains a collection of assets used across the Jetpack XR
+libraries for testing/sample purposes. Additional assets may not be added in
+other repositories to avoid file duplication.
+
+All files in this directory were created for this purpose and are released
+under the terms of the Apache 2.0 license.
diff --git a/xr/assets/build.gradle b/xr/assets/build.gradle
new file mode 100644
index 0000000..0116f70
--- /dev/null
+++ b/xr/assets/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    namespace = "androidx.xr.assets"
+}
+
+androidx {
+    name = "XR Assets"
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+    inceptionYear = "2024"
+    description = "Collection of assets for use across the XR libraries."
+}
diff --git a/xr/assets/src/main/assets/models/xyzArrows.glb b/xr/assets/src/main/assets/models/xyzArrows.glb
new file mode 100644
index 0000000..c43f3e7
--- /dev/null
+++ b/xr/assets/src/main/assets/models/xyzArrows.glb
Binary files differ