Merge "Remove paddings and insets from scaffold APIs" into androidx-main
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 733504c..2af4575 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,22 +1,22 @@
 # Global owners:
 # Unless a later match takes precedence, global owners will be # requested for
 # review when someone opens a pull request.
-*          @dlam @yigit
+*                  @dlam @yigit
 
 # Owners for each library group:
-/activity*          @jbw0033 @ianhanniballake
-/appcompat/*        @alanv
-/biometric/*        @jbolinger
-/collection/*       @dlam
-/compose/compiler/* @jimgoog @lelandrichardson
-/compose/runtime/*  @jimgoog @lelandrichardson
-/core/*             @alanv
-/datastore/*        @rohitsat13 @yigit
-/fragment/*         @jbw0033 @ianhanniballake
-/lifecycle/*        @jbw0033 @ianhanniballake
-/navigation/*       @jbw0033 @ianhanniballake @claraf3
-/paging/*           @claraf3 @ianhanniballake
-/room/*             @droid-wan-kenobi @danysantiago @svasilinets
-/work/*             @svasilinets @tikurahul
-
+/activity/         @jbw0033 @ianhanniballake
+/annotation/       @jbw0033 @ianhanniballake
+/appcompat/        @alanv
+/biometric/        @jbolinger
+/collection/       @dlam
+/compose/compiler/ @jimgoog @lelandrichardson
+/compose/runtime/  @jimgoog @lelandrichardson
+/core/             @alanv
+/datastore/        @rohitsat13 @yigit
+/fragment/         @jbw0033 @ianhanniballake
+/lifecycle/        @jbw0033 @ianhanniballake
+/navigation/       @jbw0033 @ianhanniballake @claraf3
+/paging/           @claraf3 @ianhanniballake
+/room/             @droid-wan-kenobi @danysantiago @svasilinets
+/work/             @svasilinets @tikurahul
 
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
index def0983..1d6776c 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
@@ -24,12 +24,12 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
index 4af3fc8..e7bf0f8 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
@@ -29,12 +29,12 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
diff --git a/activity/activity/api/1.9.0-beta01.txt b/activity/activity/api/1.9.0-beta01.txt
index a4c9c09..a7116a8 100644
--- a/activity/activity/api/1.9.0-beta01.txt
+++ b/activity/activity/api/1.9.0-beta01.txt
@@ -346,7 +346,7 @@
     method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri> {
+  public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
     ctor @Deprecated public ActivityResultContracts.CreateDocument();
     ctor public ActivityResultContracts.CreateDocument(String mimeType);
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
@@ -354,7 +354,7 @@
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri> {
+  public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
     ctor public ActivityResultContracts.GetContent();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String input);
@@ -368,14 +368,14 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri> {
+  public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri?> {
     ctor public ActivityResultContracts.OpenDocument();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String[] input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String[] input);
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.net.Uri> {
+  @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri?,android.net.Uri?> {
     ctor public ActivityResultContracts.OpenDocumentTree();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri? input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, android.net.Uri? input);
@@ -389,7 +389,7 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void,android.net.Uri> {
+  public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.net.Uri?> {
     ctor public ActivityResultContracts.PickContact();
     method public android.content.Intent createIntent(android.content.Context context, Void? input);
     method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
@@ -402,7 +402,7 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri?> {
     ctor public ActivityResultContracts.PickVisualMedia();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
@@ -492,14 +492,14 @@
     method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void,android.graphics.Bitmap> {
+  public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.graphics.Bitmap?> {
     ctor public ActivityResultContracts.TakePicturePreview();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, Void? input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, Void? input);
     method public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap> {
+  @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap?> {
     ctor @Deprecated public ActivityResultContracts.TakeVideo();
     method @Deprecated @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
     method @Deprecated public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, android.net.Uri input);
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index a4c9c09..a7116a8 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -346,7 +346,7 @@
     method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri> {
+  public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
     ctor @Deprecated public ActivityResultContracts.CreateDocument();
     ctor public ActivityResultContracts.CreateDocument(String mimeType);
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
@@ -354,7 +354,7 @@
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri> {
+  public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
     ctor public ActivityResultContracts.GetContent();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String input);
@@ -368,14 +368,14 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri> {
+  public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri?> {
     ctor public ActivityResultContracts.OpenDocument();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String[] input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String[] input);
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.net.Uri> {
+  @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri?,android.net.Uri?> {
     ctor public ActivityResultContracts.OpenDocumentTree();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri? input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, android.net.Uri? input);
@@ -389,7 +389,7 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void,android.net.Uri> {
+  public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.net.Uri?> {
     ctor public ActivityResultContracts.PickContact();
     method public android.content.Intent createIntent(android.content.Context context, Void? input);
     method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
@@ -402,7 +402,7 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri?> {
     ctor public ActivityResultContracts.PickVisualMedia();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
@@ -492,14 +492,14 @@
     method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void,android.graphics.Bitmap> {
+  public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.graphics.Bitmap?> {
     ctor public ActivityResultContracts.TakePicturePreview();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, Void? input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, Void? input);
     method public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap> {
+  @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap?> {
     ctor @Deprecated public ActivityResultContracts.TakeVideo();
     method @Deprecated @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
     method @Deprecated public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, android.net.Uri input);
diff --git a/activity/activity/api/restricted_1.9.0-beta01.txt b/activity/activity/api/restricted_1.9.0-beta01.txt
index 3cb9aa0..3179bef 100644
--- a/activity/activity/api/restricted_1.9.0-beta01.txt
+++ b/activity/activity/api/restricted_1.9.0-beta01.txt
@@ -345,7 +345,7 @@
     method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri> {
+  public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
     ctor @Deprecated public ActivityResultContracts.CreateDocument();
     ctor public ActivityResultContracts.CreateDocument(String mimeType);
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
@@ -353,7 +353,7 @@
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri> {
+  public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
     ctor public ActivityResultContracts.GetContent();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String input);
@@ -367,14 +367,14 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri> {
+  public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri?> {
     ctor public ActivityResultContracts.OpenDocument();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String[] input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String[] input);
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.net.Uri> {
+  @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri?,android.net.Uri?> {
     ctor public ActivityResultContracts.OpenDocumentTree();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri? input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, android.net.Uri? input);
@@ -388,7 +388,7 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void,android.net.Uri> {
+  public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.net.Uri?> {
     ctor public ActivityResultContracts.PickContact();
     method public android.content.Intent createIntent(android.content.Context context, Void? input);
     method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
@@ -401,7 +401,7 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri?> {
     ctor public ActivityResultContracts.PickVisualMedia();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
@@ -491,14 +491,14 @@
     method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void,android.graphics.Bitmap> {
+  public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.graphics.Bitmap?> {
     ctor public ActivityResultContracts.TakePicturePreview();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, Void? input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, Void? input);
     method public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap> {
+  @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap?> {
     ctor @Deprecated public ActivityResultContracts.TakeVideo();
     method @Deprecated @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
     method @Deprecated public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, android.net.Uri input);
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 3cb9aa0..3179bef 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -345,7 +345,7 @@
     method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri> {
+  public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
     ctor @Deprecated public ActivityResultContracts.CreateDocument();
     ctor public ActivityResultContracts.CreateDocument(String mimeType);
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
@@ -353,7 +353,7 @@
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri> {
+  public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
     ctor public ActivityResultContracts.GetContent();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String input);
@@ -367,14 +367,14 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri> {
+  public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri?> {
     ctor public ActivityResultContracts.OpenDocument();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, String[] input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String[] input);
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.net.Uri> {
+  @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri?,android.net.Uri?> {
     ctor public ActivityResultContracts.OpenDocumentTree();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri? input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, android.net.Uri? input);
@@ -388,7 +388,7 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void,android.net.Uri> {
+  public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.net.Uri?> {
     ctor public ActivityResultContracts.PickContact();
     method public android.content.Intent createIntent(android.content.Context context, Void? input);
     method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
@@ -401,7 +401,7 @@
     method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri?> {
     ctor public ActivityResultContracts.PickVisualMedia();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
@@ -491,14 +491,14 @@
     method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void,android.graphics.Bitmap> {
+  public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.graphics.Bitmap?> {
     ctor public ActivityResultContracts.TakePicturePreview();
     method @CallSuper public android.content.Intent createIntent(android.content.Context context, Void? input);
     method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, Void? input);
     method public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
   }
 
-  @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap> {
+  @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap?> {
     ctor @Deprecated public ActivityResultContracts.TakeVideo();
     method @Deprecated @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
     method @Deprecated public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, android.net.Uri input);
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
index 50643b2..0b9a0bc 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
@@ -49,7 +49,7 @@
     private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
         override fun visitAnnotation(node: UAnnotation) {
             val annotated = node.uastParent as? UAnnotated ?: return
-            val isKotlin = isKotlin(annotated.sourcePsi)
+            val isKotlin = isKotlin(annotated.lang)
             val qualifiedName = node.qualifiedName
 
             if (isKotlin && qualifiedName == JAVA_REQUIRES_OPT_IN_ANNOTATION) {
@@ -69,7 +69,7 @@
          * if it does not.
          */
         private fun validateAnnotationRetention(annotated: UAnnotated) {
-            val isKotlin = isKotlin(annotated.sourcePsi)
+            val isKotlin = isKotlin(annotated.lang)
             val annotations = context.evaluator.getAllAnnotations(annotated, false)
 
             val annotationClass: String
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
index 719a480..7b3fab7 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
@@ -473,7 +473,7 @@
                 // compiler handles that already. Allow either Java or Kotlin annotations, since
                 // we can enforce both and it's possible that a Kotlin-sourced experimental library
                 // is being used from Java without the Kotlin stdlib in the classpath.
-                if (!isKotlin(usage.sourcePsi)) {
+                if (!isKotlin(usage.lang)) {
                     checkExperimentalUsage(
                         context,
                         annotation,
@@ -633,7 +633,7 @@
         val lintFixes = fix().alternatives()
         var addedFix = false
         usage.getContainingUMethod()?.let { containingMethod ->
-            val isKotlin = isKotlin(usage.sourcePsi)
+            val isKotlin = isKotlin(usage.lang)
             val optInAnnotation = if (isKotlin) {
                 "@androidx.annotation.OptIn($annotation::class)"
             } else {
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index c814811..c221d9e 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -45,7 +45,7 @@
     method public abstract Class<? extends androidx.appsearch.app.LongSerializer<?>!> serializer() default androidx.appsearch.annotation.Document.LongProperty.DefaultSerializer.class;
   }
 
-  public static final class Document.LongProperty.DefaultSerializer implements androidx.appsearch.app.LongSerializer<java.lang.Long> {
+  public static final class Document.LongProperty.DefaultSerializer implements androidx.appsearch.app.LongSerializer<java.lang.Long!> {
     ctor public Document.LongProperty.DefaultSerializer();
     method public Long deserialize(long);
     method public long serialize(Long);
@@ -66,7 +66,7 @@
     method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
   }
 
-  public static final class Document.StringProperty.DefaultSerializer implements androidx.appsearch.app.StringSerializer<java.lang.String> {
+  public static final class Document.StringProperty.DefaultSerializer implements androidx.appsearch.app.StringSerializer<java.lang.String!> {
     ctor public Document.StringProperty.DefaultSerializer();
     method public String deserialize(String);
     method public String serialize(String);
@@ -405,7 +405,7 @@
     method public byte[] getSha256Certificate();
   }
 
-  public class PropertyPath implements java.lang.Iterable<androidx.appsearch.app.PropertyPath.PathSegment> {
+  public class PropertyPath implements java.lang.Iterable<androidx.appsearch.app.PropertyPath.PathSegment!> {
     ctor public PropertyPath(String);
     ctor public PropertyPath(java.util.List<androidx.appsearch.app.PropertyPath.PathSegment!>);
     method public androidx.appsearch.app.PropertyPath.PathSegment get(int);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index c814811..c221d9e 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -45,7 +45,7 @@
     method public abstract Class<? extends androidx.appsearch.app.LongSerializer<?>!> serializer() default androidx.appsearch.annotation.Document.LongProperty.DefaultSerializer.class;
   }
 
-  public static final class Document.LongProperty.DefaultSerializer implements androidx.appsearch.app.LongSerializer<java.lang.Long> {
+  public static final class Document.LongProperty.DefaultSerializer implements androidx.appsearch.app.LongSerializer<java.lang.Long!> {
     ctor public Document.LongProperty.DefaultSerializer();
     method public Long deserialize(long);
     method public long serialize(Long);
@@ -66,7 +66,7 @@
     method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
   }
 
-  public static final class Document.StringProperty.DefaultSerializer implements androidx.appsearch.app.StringSerializer<java.lang.String> {
+  public static final class Document.StringProperty.DefaultSerializer implements androidx.appsearch.app.StringSerializer<java.lang.String!> {
     ctor public Document.StringProperty.DefaultSerializer();
     method public String deserialize(String);
     method public String serialize(String);
@@ -405,7 +405,7 @@
     method public byte[] getSha256Certificate();
   }
 
-  public class PropertyPath implements java.lang.Iterable<androidx.appsearch.app.PropertyPath.PathSegment> {
+  public class PropertyPath implements java.lang.Iterable<androidx.appsearch.app.PropertyPath.PathSegment!> {
     ctor public PropertyPath(String);
     ctor public PropertyPath(java.util.List<androidx.appsearch.app.PropertyPath.PathSegment!>);
     method public androidx.appsearch.app.PropertyPath.PathSegment get(int);
diff --git a/arch/core/core-common/api/restricted_current.txt b/arch/core/core-common/api/restricted_current.txt
index 4fbc435..1ef0e0f 100644
--- a/arch/core/core-common/api/restricted_current.txt
+++ b/arch/core/core-common/api/restricted_current.txt
@@ -1,13 +1,13 @@
 // Signature format: 4.0
 package androidx.arch.core.internal {
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FastSafeIterableMap<K, V> extends androidx.arch.core.internal.SafeIterableMap<K,V> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FastSafeIterableMap<K, V> extends androidx.arch.core.internal.SafeIterableMap<K!,V!> {
     ctor public FastSafeIterableMap();
     method public java.util.Map.Entry<K!,V!>? ceil(K!);
     method public boolean contains(K!);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap<K, V> implements java.lang.Iterable<java.util.Map.Entry<K,V>> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap<K, V> implements java.lang.Iterable<java.util.Map.Entry<K!,V!>!> {
     ctor public SafeIterableMap();
     method public java.util.Iterator<java.util.Map.Entry<K!,V!>!> descendingIterator();
     method public java.util.Map.Entry<K!,V!>? eldest();
@@ -20,7 +20,7 @@
     method public int size();
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove<K,V> implements java.util.Iterator<java.util.Map.Entry<K,V>> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove<K!,V!> implements java.util.Iterator<java.util.Map.Entry<K!,V!>!> {
     method public boolean hasNext();
     method public java.util.Map.Entry<K!,V!>! next();
   }
diff --git a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
index 5c2583a..6db0d62 100644
--- a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
+++ b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha09" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha09)" variant="all" version="8.4.0-alpha09">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
 
     <issue
-        id="InternalGradleApiUsage"
+        id="InternalAgpApiUsage"
         message="Avoid using internal Android Gradle Plugin APIs"
         errorLine1="import com.android.build.gradle.internal.api.DefaultAndroidSourceDirectorySet"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -11,7 +11,7 @@
     </issue>
 
     <issue
-        id="InternalGradleApiUsage"
+        id="InternalAgpApiUsage"
         message="Avoid using internal Android Gradle Plugin APIs"
         errorLine1="import com.android.build.gradle.internal.api.DefaultAndroidSourceFile"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -20,7 +20,7 @@
     </issue>
 
     <issue
-        id="InternalGradleApiUsage"
+        id="InternalAgpApiUsage"
         message="Avoid using internal Android Gradle Plugin APIs"
         errorLine1="import com.android.build.gradle.internal.tasks.BuildAnalyzer"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -29,7 +29,7 @@
     </issue>
 
     <issue
-        id="InternalGradleApiUsage"
+        id="InternalAgpApiUsage"
         message="Avoid using internal Android Gradle Plugin APIs"
         errorLine1="import com.android.build.gradle.internal.tasks.BuildAnalyzer"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -38,7 +38,7 @@
     </issue>
 
     <issue
-        id="InternalGradleApiUsage"
+        id="InternalAgpApiUsage"
         message="Avoid using internal Android Gradle Plugin APIs"
         errorLine1="import com.android.build.gradle.internal.tasks.BuildAnalyzer"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -47,7 +47,7 @@
     </issue>
 
     <issue
-        id="InternalGradleApiUsage"
+        id="InternalAgpApiUsage"
         message="Avoid using internal Android Gradle Plugin APIs"
         errorLine1="import com.android.build.gradle.internal.tasks.BuildAnalyzer"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
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 08a4c83..d87b51a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -261,6 +261,18 @@
         runOnMainDeadlineSeconds =
             arguments.getBenchmarkArgument("runOnMainDeadlineSeconds")?.toLong() ?: 30
         Log.d(BenchmarkState.TAG, "runOnMainDeadlineSeconds $runOnMainDeadlineSeconds")
+
+        if (arguments.getString("orchestratorService") != null) {
+            InstrumentationResults.scheduleIdeWarningOnNextReport(
+                """
+                    AndroidX Benchmark does not support running with the AndroidX Test Orchestrator.
+
+                    AndroidX benchmarks (micro and macro) produce one JSON file per test module,
+                    which together with Test Orchestrator restarting the process frequently causes
+                    benchmark output JSON files to be repeatedly overwritten during the test.
+                    """.trimIndent()
+            )
+        }
     }
 
     fun macrobenchMethodTracingEnabled(): Boolean {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
index 32e3e54..3956071 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import androidx.annotation.RestrictTo
 import androidx.test.platform.app.InstrumentationRegistry
+import java.util.Locale
 import org.jetbrains.annotations.TestOnly
 
 /**
@@ -139,14 +140,14 @@
         // for readability, report nanos with 10ths only if less than 100
         var output = if (nanos >= 100.0) {
             // 13 alignment is enough for ~10 seconds
-            "%,13d   ns".format(nanos.toLong())
+            "%,13d   ns".format(Locale.US, nanos.toLong())
         } else {
             // 13 + 2(.X) to match alignment above
-            "%,15.1f ns".format(nanos)
+            "%,15.1f ns".format(Locale.US, nanos)
         }
         if (allocations != null) {
             // 9 alignment is enough for ~10 million allocations
-            output += "    %8d allocs".format(allocations.toInt())
+            output += "    %8d allocs".format(Locale.US, allocations.toInt())
         }
         profilerResults.forEach {
             output += "    [${it.label}](file://${it.sanitizedOutputRelativePath})"
@@ -234,7 +235,7 @@
 
             val allMetrics = measurements.singleMetrics + measurements.sampledMetrics
             val maxLabelLength = allMetrics.maxOf { it.name.length }
-            fun Double.toDisplayString() = "%,.1f".format(this)
+            fun Double.toDisplayString() = "%,.1f".format(Locale.US, this)
 
             // max string length of any printed min/med/max is the largest max value seen. used to pad.
             val maxValueLength = allMetrics
diff --git a/benchmark/benchmark-macro/api/current.txt b/benchmark/benchmark-macro/api/current.txt
index b9b8d3b..b70d0d0 100644
--- a/benchmark/benchmark-macro/api/current.txt
+++ b/benchmark/benchmark-macro/api/current.txt
@@ -72,12 +72,12 @@
 
   @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryCountersMetric extends androidx.benchmark.macro.TraceMetric {
     ctor public MemoryCountersMetric();
-    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getResult(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
   }
 
   @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryUsageMetric extends androidx.benchmark.macro.TraceMetric {
     ctor public MemoryUsageMetric(androidx.benchmark.macro.MemoryUsageMetric.Mode mode, optional java.util.List<? extends androidx.benchmark.macro.MemoryUsageMetric.SubMetric> subMetrics);
-    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getResult(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
   }
 
   public enum MemoryUsageMetric.Mode {
@@ -192,7 +192,7 @@
 
   @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public abstract class TraceMetric extends androidx.benchmark.macro.Metric {
     ctor public TraceMetric();
-    method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getResult(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+    method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
   }
 
   @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
@@ -228,10 +228,13 @@
 
   public static final class PerfettoTraceProcessor.Session {
     method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(@org.intellij.lang.annotations.Language("sql") String query);
+    method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
+    method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
+    method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
     method public byte[] rawQuery(@org.intellij.lang.annotations.Language("sql") String query);
   }
 
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object> {
+  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
     ctor public Row(java.util.Map<java.lang.String,?> map);
     method public byte[] bytes(String columnName);
     method public double double(String columnName);
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index f98e021..220407a 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -85,12 +85,12 @@
 
   @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryCountersMetric extends androidx.benchmark.macro.TraceMetric {
     ctor public MemoryCountersMetric();
-    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getResult(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
   }
 
   @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryUsageMetric extends androidx.benchmark.macro.TraceMetric {
     ctor public MemoryUsageMetric(androidx.benchmark.macro.MemoryUsageMetric.Mode mode, optional java.util.List<? extends androidx.benchmark.macro.MemoryUsageMetric.SubMetric> subMetrics);
-    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getResult(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
   }
 
   public enum MemoryUsageMetric.Mode {
@@ -214,7 +214,7 @@
 
   @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public abstract class TraceMetric extends androidx.benchmark.macro.Metric {
     ctor public TraceMetric();
-    method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getResult(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+    method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
   }
 
   @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
@@ -250,10 +250,13 @@
 
   public static final class PerfettoTraceProcessor.Session {
     method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(@org.intellij.lang.annotations.Language("sql") String query);
+    method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
+    method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
+    method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
     method public byte[] rawQuery(@org.intellij.lang.annotations.Language("sql") String query);
   }
 
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object> {
+  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
     ctor public Row(java.util.Map<java.lang.String,?> map);
     method public byte[] bytes(String columnName);
     method public double double(String columnName);
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
index 7f1fa9a..b47435e 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
@@ -44,7 +44,7 @@
             .associateWith { PowerCategoryDisplayLevel.BREAKDOWN }
 
         val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
-            PowerMetric(PowerMetric.Energy(categories)).getResult(captureInfo, this)
+            PowerMetric(PowerMetric.Energy(categories)).getMeasurements(captureInfo, this)
         }
 
         assertEqualMeasurements(
@@ -82,7 +82,7 @@
             .associateWith { PowerCategoryDisplayLevel.TOTAL }
 
         val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
-            PowerMetric(PowerMetric.Power(categories)).getResult(captureInfo, this)
+            PowerMetric(PowerMetric.Power(categories)).getMeasurements(captureInfo, this)
         }
 
         assertEqualMeasurements(
@@ -116,7 +116,7 @@
         )
 
         val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
-            PowerMetric(PowerMetric.Power(categories)).getResult(captureInfo, this)
+            PowerMetric(PowerMetric.Power(categories)).getMeasurements(captureInfo, this)
         }
 
         assertEqualMeasurements(
@@ -144,7 +144,7 @@
             .associateWith { PowerCategoryDisplayLevel.BREAKDOWN }
 
         val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
-            PowerMetric(PowerMetric.Energy(categories)).getResult(captureInfo, this)
+            PowerMetric(PowerMetric.Energy(categories)).getMeasurements(captureInfo, this)
         }
 
         assertEquals(emptyList(), actualMetrics)
@@ -158,7 +158,7 @@
         val traceFile = createTempFileFromAsset("api31_battery_discharge", ".perfetto-trace")
 
         val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
-            PowerMetric(PowerMetric.Battery()).getResult(captureInfo, this)
+            PowerMetric(PowerMetric.Battery()).getMeasurements(captureInfo, this)
         }
 
         assertEqualMeasurements(
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index 719e6a8..04dda73 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -215,7 +215,7 @@
 
         metric.configure(packageName)
         return PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
-            metric.getResult(
+            metric.getMeasurements(
                 captureInfo = Metric.CaptureInfo(
                     targetPackageName = "androidx.benchmark.integration.macrobenchmark.target",
                     testPackageName = "androidx.benchmark.integration.macrobenchmark.test",
@@ -239,7 +239,7 @@
         metric.configure(Packages.TEST)
 
         val measurements = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
-            metric.getResult(
+            metric.getMeasurements(
                 captureInfo = Metric.CaptureInfo(
                     targetPackageName = Packages.TEST,
                     testPackageName = Packages.TEST,
@@ -317,7 +317,7 @@
     )!!
 
     return PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
-        metric.getResult(
+        metric.getMeasurements(
             captureInfo = Metric.CaptureInfo(
                 targetPackageName = packageName,
                 testPackageName = Packages.TEST,
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceMetricTest.kt
index da73a55..2ca7cbc 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceMetricTest.kt
@@ -37,7 +37,7 @@
     )
 
     class ActivityResumeMetric : TraceMetric() {
-        override fun getResult(
+        override fun getMeasurements(
             captureInfo: CaptureInfo,
             traceSession: PerfettoTraceProcessor.Session
         ): List<Measurement> {
@@ -84,7 +84,7 @@
             metric.configure(packageName = Packages.TEST)
 
             val result = PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
-                metric.getResult(
+                metric.getMeasurements(
                     captureInfo = captureInfo,
                     traceSession = this
                 )
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
index ee4b593..7e68b4c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
@@ -118,7 +118,7 @@
             metric.configure(packageName = packageName)
 
             val result = PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
-                metric.getResult(
+                metric.getMeasurements(
                     // note that most args are incorrect here, but currently
                     // only targetPackageName matters in this context
                     captureInfo = Metric.CaptureInfo(
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
index 021fc1c..014480c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
@@ -38,6 +38,7 @@
 import org.junit.Assume.assumeTrue
 import org.junit.Test
 import org.junit.runner.RunWith
+import perfetto.protos.TraceMetrics
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -249,6 +250,61 @@
     }
 
     @Test
+    fun query_includeModule() {
+        assumeTrue(isAbiSupported())
+        val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+        val startups = PerfettoTraceProcessor.runServer {
+            loadTrace(PerfettoTrace(traceFile.absolutePath)) {
+                query("""
+                    INCLUDE PERFETTO MODULE android.startup.startups;
+
+                    SELECT * FROM android_startups;
+                """.trimIndent()).toList()
+            }
+        }
+        // minimal validation, just verifying query worked
+        assertEquals(1, startups.size)
+        assertEquals(
+            "androidx.benchmark.integration.macrobenchmark.target",
+            startups.single().string("package")
+        )
+    }
+
+    @Test
+    fun queryMetricsJson() {
+        assumeTrue(isAbiSupported())
+        val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+        PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+            val metrics = queryMetricsJson(listOf("android_startup"))
+            assertTrue(metrics.contains("\"android_startup\": {"))
+            assertTrue(metrics.contains("\"startup_type\": \"cold\","))
+        }
+    }
+
+    @Test
+    fun queryMetricsProtoBinary() {
+        assumeTrue(isAbiSupported())
+        val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+        PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+            val metrics =
+                TraceMetrics.ADAPTER.decode(queryMetricsProtoBinary(listOf("android_startup")))
+            val startup = metrics.android_startup!!
+            assertEquals(startup.startup.single().startup_type, "cold")
+        }
+    }
+
+    @Test
+    fun queryMetricsProtoText() {
+        assumeTrue(isAbiSupported())
+        val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+        PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+            val metrics = queryMetricsProtoText(listOf("android_startup"))
+            assertTrue(metrics.contains("android_startup {"))
+            assertTrue(metrics.contains("startup_type: \"cold\""))
+        }
+    }
+
+    @Test
     fun validatePerfettoTraceProcessorBinariesExist() {
         val context = InstrumentationRegistry.getInstrumentation().targetContext
         val suffixes = listOf("aarch64")
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 6d83128..a1bb59d 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -326,7 +326,7 @@
                         metrics
                             // capture list of Measurements
                             .map {
-                                it.getResult(
+                                it.getMeasurements(
                                     Metric.CaptureInfo(
                                         targetPackageName = packageName,
                                         testPackageName = macrobenchPackageName,
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 260e340..522c310 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -52,7 +52,7 @@
      * TODO: takes package for package level filtering, but probably want a
      *  general config object coming into [start].
      */
-    internal abstract fun getResult(
+    internal abstract fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement>
@@ -137,7 +137,18 @@
  * how much faster than the deadline a frame was.
  *
  * * `frameDurationCpuMs` - How much time the frame took to be produced on the CPU - on both the UI
- * Thread, and RenderThread.
+ * Thread, and RenderThread. Note that this doesn't account for time before the frame started
+ * (before Choreographer#doFrame), as that data isn't available in traces prior to API 31.
+ *
+ * * `frameCount` - How many total frames were produced. This is a secondary metric which can be
+ * used to understand *why* the above metrics changed. For example, when removing unneeded frames
+ * that were incorrectly invalidated to save power, `frameOverrunMs` and `frameDurationCpuMs` will
+ * often get worse, as the removed frames were trivial. Checking `frameCount` can be a useful
+ * indicator in such cases.
+ *
+ * Generally, prefer tracking and detecting regressions with `frameOverrunMs` when it is available,
+ * as it is the more complete data, and accounts for modern devices (including higher, variable
+ * framerate rendering) more naturally.
  */
 @Suppress("CanSealedSubClassBeObject")
 class FrameTimingMetric : Metric() {
@@ -145,15 +156,16 @@
     override fun start() {}
     override fun stop() {}
 
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
-        return FrameTimingQuery.getFrameData(
+        val frameData = FrameTimingQuery.getFrameData(
             session = traceSession,
             captureApiLevel = captureInfo.apiLevel,
             packageName = captureInfo.targetPackageName
         )
+        return frameData
             .getFrameSubMetrics(captureInfo.apiLevel)
             .filterKeys { it == SubMetric.FrameDurationCpuNs || it == SubMetric.FrameOverrunNs }
             .map {
@@ -165,7 +177,7 @@
                     },
                     dataSamples = it.value.map { timeNs -> timeNs.nsToDoubleMs() }
                 )
-            }
+            } + listOf(Measurement("frameCount", frameData.size.toDouble()))
     }
 }
 
@@ -260,7 +272,7 @@
         "gfxFrameJankPercent",
     )
 
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
@@ -303,7 +315,7 @@
     override fun stop() {
     }
 
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
@@ -343,7 +355,7 @@
     override fun stop() {
     }
 
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
@@ -386,10 +398,10 @@
  * package:
  * ```
  * class ActivityResumeMetric : TraceMetric() {
- *     override fun getResult(
+ *     override fun getMeasurements(
  *         captureInfo: CaptureInfo,
  *         traceSession: PerfettoTraceProcessor.Session
- *     ): Result {
+ *     ): List<Measurement> {
  *         val rowSequence = traceSession.query(
  *             """
  *             SELECT
@@ -409,9 +421,9 @@
  *         // to capture timing of every component of activity lifecycle
  *         val activityResultNs = rowSequence.firstOrNull()?.double("dur")
  *         return if (activityResultMs != null) {
- *             Result("activityResumeMs", activityResultNs / 1_000_000.0)
+ *             listOf(Measurement("activityResumeMs", activityResultNs / 1_000_000.0))
  *         } else {
- *             Result()
+ *             emptyList()
  *         }
  *     }
  * }
@@ -435,7 +447,7 @@
     /**
      * Get the metric result for a given iteration given information about the target process and a TraceProcessor session
      */
-    public abstract override fun getResult(
+    public abstract override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement>
@@ -521,7 +533,7 @@
     override fun stop() {
     }
 
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
@@ -700,7 +712,7 @@
         }
     }
 
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
@@ -860,7 +872,7 @@
         Gpu("GPU Memory", alreadyInKb = false)
     }
 
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
@@ -885,7 +897,7 @@
  */
 @ExperimentalMetricApi
 class MemoryCountersMetric : TraceMetric() {
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
index 1fa38b8..6960117 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
@@ -210,11 +210,16 @@
     /**
      * Computes the given metrics on a previously parsed trace.
      */
-    fun computeMetric(metrics: List<String>): ComputeMetricResult =
+    fun computeMetric(
+        metrics: List<String>,
+        resultFormat: ComputeMetricArgs.ResultFormat
+    ): ComputeMetricResult =
         httpRequest(
             method = METHOD_POST,
             url = PATH_COMPUTE_METRIC,
-            encodeBlock = { ComputeMetricArgs.ADAPTER.encode(it, ComputeMetricArgs(metrics)) },
+            encodeBlock = {
+                ComputeMetricArgs.ADAPTER.encode(it, ComputeMetricArgs(metrics, resultFormat))
+            },
             decodeBlock = { ComputeMetricResult.ADAPTER.decode(it) }
         )
 
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
index af808e9..1960762 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
@@ -29,6 +29,8 @@
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.seconds
 import org.intellij.lang.annotations.Language
+import perfetto.protos.ComputeMetricArgs
+import perfetto.protos.ComputeMetricResult
 import perfetto.protos.QueryResult
 import perfetto.protos.TraceMetrics
 
@@ -196,22 +198,87 @@
          */
         @RestrictTo(LIBRARY_GROUP) // avoids exposing Proto API
         fun getTraceMetrics(metric: String): TraceMetrics {
-            inMemoryTrace("PerfettoTraceProcessor#getTraceMetrics $metric") {
-                require(!metric.contains(" ")) {
-                    "Metric must not contain spaces: $metric"
-                }
+            val computeResult = queryAndVerifyMetricResult(
+                listOf(metric),
+                ComputeMetricArgs.ResultFormat.BINARY_PROTOBUF
+            )
+            return TraceMetrics.ADAPTER.decode(computeResult.metrics!!)
+        }
+
+        /**
+         * Computes the given metrics, returning the results as a binary proto.
+         *
+         * The proto format definition for decoding this binary format can be found
+         * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
+         *
+         * See [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
+         * for an overview on trace based metrics.
+         */
+        fun queryMetricsProtoBinary(metrics: List<String>): ByteArray {
+            val computeResult = queryAndVerifyMetricResult(
+                metrics,
+                ComputeMetricArgs.ResultFormat.BINARY_PROTOBUF
+            )
+            return computeResult.metrics!!.toByteArray()
+        }
+
+        /**
+         * Computes the given metrics, returning the results as JSON text.
+         *
+         * The proto format definition for these metrics can be found
+         * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
+         *
+         * See [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
+         * for an overview on trace based metrics.
+         */
+        fun queryMetricsJson(metrics: List<String>): String {
+            val computeResult = queryAndVerifyMetricResult(
+                metrics,
+                ComputeMetricArgs.ResultFormat.JSON
+            )
+            check(computeResult.metrics_as_json != null)
+            return computeResult.metrics_as_json
+        }
+
+        /**
+         * Computes the given metrics, returning the result as proto text.
+         *
+         * The proto format definition for these metrics can be found
+         * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
+         *
+         * See [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
+         * for an overview on trace based metrics.
+         */
+        fun queryMetricsProtoText(metrics: List<String>): String {
+            val computeResult = queryAndVerifyMetricResult(
+                metrics,
+                ComputeMetricArgs.ResultFormat.TEXTPROTO
+            )
+            check(computeResult.metrics_as_prototext != null)
+            return computeResult.metrics_as_prototext
+        }
+
+        private fun queryAndVerifyMetricResult(
+            metrics: List<String>,
+            format: ComputeMetricArgs.ResultFormat
+        ): ComputeMetricResult {
+            val nameString = metrics.joinToString()
+            require(metrics.none { it.contains(" ") }) {
+                "Metrics must not constain spaces, metrics: $nameString"
+            }
+
+            inMemoryTrace("PerfettoTraceProcessor#getTraceMetrics $nameString") {
                 require(traceProcessor.perfettoHttpServer.isRunning()) {
                     "Perfetto trace_shell_process is not running."
                 }
 
                 // Compute metrics
-                val computeResult = traceProcessor.perfettoHttpServer.computeMetric(listOf(metric))
+                val computeResult = traceProcessor.perfettoHttpServer.computeMetric(metrics, format)
                 if (computeResult.error != null) {
                     throw IllegalStateException(computeResult.error)
                 }
 
-                // Decode and return trace metrics
-                return TraceMetrics.ADAPTER.decode(computeResult.metrics!!)
+                return computeResult
             }
         }
 
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 82349a5..935864f4 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha09" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha09)" variant="all" version="8.4.0-alpha09">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
 
     <issue
         id="EagerGradleConfiguration"
@@ -184,15 +184,6 @@
     <issue
         id="EagerGradleConfiguration"
         message="Avoid using eager method get"
-        errorLine1="                        .get() as ProcessLibraryManifest"
-        errorLine2="                         ~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/checkapi/ApiTasks.kt"/>
-    </issue>
-
-    <issue
-        id="EagerGradleConfiguration"
-        message="Avoid using eager method get"
         errorLine1="        val jvmJarTask = jvmJarTaskProvider.get()"
         errorLine2="                                            ~~~">
         <location
@@ -353,6 +344,60 @@
     </issue>
 
     <issue
+        id="InternalAgpApiUsage"
+        message="Avoid using internal Android Gradle Plugin APIs"
+        errorLine1="import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="InternalAgpApiUsage"
+        message="Avoid using internal Android Gradle Plugin APIs"
+        errorLine1="import com.android.build.gradle.internal.lint.LintModelWriterTask"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="InternalAgpApiUsage"
+        message="Avoid using internal Android Gradle Plugin APIs"
+        errorLine1="import com.android.build.gradle.internal.lint.VariantInputs"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="InternalAgpApiUsage"
+        message="Avoid using internal Android Gradle Plugin APIs"
+        errorLine1="import com.android.build.gradle.internal.attributes.VariantAttr"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="InternalAgpApiUsage"
+        message="Avoid using internal Android Gradle Plugin APIs"
+        errorLine1="import com.android.build.gradle.internal.publishing.AndroidArtifacts"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="InternalAgpApiUsage"
+        message="Avoid using internal Android Gradle Plugin APIs"
+        errorLine1="import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+    </issue>
+
+    <issue
         id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
         errorLine1="import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency"
@@ -435,33 +480,6 @@
 
     <issue
         id="InternalGradleApiUsage"
-        message="Avoid using internal Android Gradle Plugin APIs"
-        errorLine1="import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
-    </issue>
-
-    <issue
-        id="InternalGradleApiUsage"
-        message="Avoid using internal Android Gradle Plugin APIs"
-        errorLine1="import com.android.build.gradle.internal.lint.LintModelWriterTask"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
-    </issue>
-
-    <issue
-        id="InternalGradleApiUsage"
-        message="Avoid using internal Android Gradle Plugin APIs"
-        errorLine1="import com.android.build.gradle.internal.lint.VariantInputs"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
-    </issue>
-
-    <issue
-        id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
         errorLine1="import org.gradle.api.internal.component.SoftwareComponentInternal"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -516,33 +534,6 @@
 
     <issue
         id="InternalGradleApiUsage"
-        message="Avoid using internal Android Gradle Plugin APIs"
-        errorLine1="import com.android.build.gradle.internal.attributes.VariantAttr"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
-    </issue>
-
-    <issue
-        id="InternalGradleApiUsage"
-        message="Avoid using internal Android Gradle Plugin APIs"
-        errorLine1="import com.android.build.gradle.internal.publishing.AndroidArtifacts"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
-    </issue>
-
-    <issue
-        id="InternalGradleApiUsage"
-        message="Avoid using internal Android Gradle Plugin APIs"
-        errorLine1="import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
-    </issue>
-
-    <issue
-        id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
         errorLine1="import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLogger"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index 3c1a750..dcdd156 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -222,7 +222,18 @@
         }
         project.dependencies.add(
             COMPILER_PLUGIN_CONFIGURATION,
-            "androidx.compose.compiler:compiler:$versionToUse"
+            if (project.isComposeCompilerUnpinned()) {
+                if (ProjectLayoutType.isPlayground(project)) {
+                    AndroidXPlaygroundRootImplPlugin.projectOrArtifact(
+                        project.rootProject,
+                        ":compose:compiler:compiler"
+                    )
+                } else {
+                    project.rootProject.resolveProject(":compose:compiler:compiler")
+                }
+            } else {
+                "androidx.compose.compiler:compiler:$versionToUse"
+            }
         )
 
         val kotlinPluginProvider = project.provider {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 5c74a44..f394fd9 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -162,12 +162,20 @@
  */
 const val INCLUDE_OPTIONAL_PROJECTS = "androidx.includeOptionalProjects"
 
+/**
+ * If true, build compose compiler from source.
+ * Should be kept to "false" unless we are upgrading the Kotlin version in order to release a new
+ * stable Compose Compiler.
+ */
+const val UNPIN_COMPOSE_COMPILER = "androidx.unpinComposeCompiler"
+
 val ALL_ANDROIDX_PROPERTIES =
     setOf(
         ADD_GROUP_CONSTRAINTS,
         ALTERNATIVE_PROJECT_URL,
         VERSION_EXTRA_CHECK_ENABLED,
         VALIDATE_PROJECT_STRUCTURE,
+        UNPIN_COMPOSE_COMPILER,
         ENABLE_COMPOSE_COMPILER_METRICS,
         ENABLE_COMPOSE_COMPILER_REPORTS,
         DISPLAY_TEST_OUTPUT,
@@ -294,6 +302,12 @@
     findBooleanProperty(ENABLE_COMPOSE_COMPILER_METRICS) ?: false
 
 /**
+ * Returns whether we export compose compiler metrics
+ */
+fun Project.isComposeCompilerUnpinned() =
+    findBooleanProperty(UNPIN_COMPOSE_COMPILER) ?: false
+
+/**
  * Returns whether we export compose compiler reports
  */
 fun Project.enableComposeCompilerReports() =
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 7c788c4..f2a02ef 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -500,7 +500,6 @@
         }
         if (plugin is KotlinMultiplatformPluginWrapper) {
             KonanPrebuiltsSetup.configureKonanDirectory(project)
-            KmpLinkTaskWorkaround.serializeLinkTasks(project)
             project.afterEvaluate {
                 val libraryExtension = project.extensions.findByType<LibraryExtension>()
                 if (libraryExtension != null) {
@@ -977,7 +976,7 @@
         defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk
         defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 
-        testOptions.animationsDisabled = true
+        testOptions.animationsDisabled = !project.isMacrobenchmark()
         testOptions.unitTests.isReturnDefaultValues = true
         testOptions.unitTests.all { task ->
             task.configureForRobolectric()
@@ -1491,6 +1490,10 @@
     return this.plugins.hasPlugin(BenchmarkPlugin::class.java)
 }
 
+fun Project.isMacrobenchmark(): Boolean {
+    return this.path.endsWith("macrobenchmark")
+}
+
 /**
  * Returns a string that is a valid filename and loosely based on the project name The value
  * returned for each project will be distinct
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
index 70fbc32..967d798 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
@@ -283,34 +283,14 @@
     fun addNativeLibrariesToJniLibs(
         androidTarget: KotlinAndroidTarget,
         nativeCompilation: MultiTargetNativeCompilation,
-        variantBuildType: String = "debug",
         forTest: Boolean = false
     ) = nativeLibraryBundler.addNativeLibrariesToJniLibs(
         androidTarget = androidTarget,
         nativeCompilation = nativeCompilation,
-        variantBuildType = variantBuildType,
         forTest = forTest
     )
 
     /**
-     * Convenience method to add native libraries to the jniLibs input of an Android instrumentation
-     * test.
-     *
-     * @see addNativeLibrariesToJniLibs
-     */
-    @JvmOverloads
-    fun addNativeLibrariesToTestJniLibs(
-        androidTarget: KotlinAndroidTarget,
-        nativeCompilation: MultiTargetNativeCompilation,
-        variantBuildType: String = "debug",
-    ) = addNativeLibrariesToJniLibs(
-        androidTarget = androidTarget,
-        nativeCompilation = nativeCompilation,
-        variantBuildType = variantBuildType,
-        forTest = true
-    )
-
-    /**
      * Convenience method to add bundle native libraries with a test jar.
      *
      * @see addNativeLibrariesToResources
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 73da6d1..2f91d77 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -128,7 +128,6 @@
 
         registerOwnersServiceTasks()
 
-        project.configureRootProjectForKmpLink()
         // If useMaxDepVersions is set, iterate through all the project and substitute any androidx
         // artifact dependency with the local tip of tree version of the library.
         if (project.usingMaxDepVersions()) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/KmpLinkTaskWorkaround.kt b/buildSrc/private/src/main/kotlin/androidx/build/KmpLinkTaskWorkaround.kt
deleted file mode 100644
index c8990fc..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/KmpLinkTaskWorkaround.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import org.gradle.api.Project
-import org.gradle.api.services.BuildService
-import org.gradle.api.services.BuildServiceParameters
-import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
-
-/**
- * Name of the service we use to limit the number of concurrent kmp link tasks
- */
-public const val KMP_LINK_SERVICE_NAME = "androidxKmpLinkService"
-
-// service for limiting the number of concurrent kmp link tasks b/309990481
-interface AndroidXKmpLinkService : BuildService<BuildServiceParameters.None>
-
-fun Project.configureRootProjectForKmpLink() {
-    project.gradle.sharedServices.registerIfAbsent(
-        KMP_LINK_SERVICE_NAME,
-        AndroidXKmpLinkService::class.java,
-        { spec ->
-            spec.maxParallelUsages.set(1)
-        }
-    )
-}
-
-object KmpLinkTaskWorkaround {
-    // b/309990481
-    fun serializeLinkTasks(
-        project: Project
-    ) {
-        project.tasks.withType(
-            KotlinNativeLink::class.java
-        ).configureEach { task ->
-            task.usesService(
-                task.project.gradle.sharedServices.registrations
-                    .getByName(KMP_LINK_SERVICE_NAME).service
-            )
-        }
-    }
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt
index 8e05eea..6a357b2 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt
@@ -146,7 +146,7 @@
             task.androidXDependencySet.set(
                 project.provider {
                     val dependencies = mutableSetOf<AndroidXDependency>()
-                    project.configurations.filter(::shouldVerifyConfiguration).forEach {
+                    project.configurations.filter(project::shouldVerifyConfiguration).forEach {
                         configuration ->
                         configuration.allDependencies.filter(::shouldVerifyDependency).forEach {
                             dependency ->
@@ -165,11 +165,12 @@
             )
             task.cacheEvenIfNoOutputs()
         }
+
     addToBuildOnServer(taskProvider)
     return taskProvider
 }
 
-private fun shouldVerifyConfiguration(configuration: Configuration): Boolean {
+private fun Project.shouldVerifyConfiguration(configuration: Configuration): Boolean {
     // Only verify configurations that are exported to POM. In an ideal world, this would be an
     // inclusion derived from the mappings used by the Maven Publish Plugin; however, since we
     // don't have direct access to those, this should remain an exclusion list.
@@ -217,11 +218,15 @@
     if (name.endsWith("DependenciesMetadata")) return false
 
     // don't verify test configurations of KMP projects
-    if (name.contains("JvmTest")) return false
-    if (name.contains("commonTest")) return false
-    if (name.contains("nativeTest")) return false
     if (name.contains("TestCompilation")) return false
     if (name.contains("TestCompile")) return false
+    if (name.contains("commonTest", ignoreCase = true)) return false
+    if (name.contains("nativeTest", ignoreCase = true)) return false
+    if (multiplatformExtension?.targets
+            ?.any { name.contains("${it.name}Test", ignoreCase = true) } == true
+    ) {
+        return false
+    }
 
     return true
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt b/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt
index 8d3766c..52a4cf2 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt
@@ -70,25 +70,22 @@
 
     /**
      * Adds the shared library outputs from [nativeCompilation] to the jni libs dependency of
-     * the [androidTarget]'s [variantBuildType].
+     * the [androidTarget].
      *
      * @see CombineObjectFilesTask for details.
      */
     fun addNativeLibrariesToJniLibs(
         androidTarget: KotlinAndroidTarget,
         nativeCompilation: MultiTargetNativeCompilation,
-        variantBuildType: String,
         forTest: Boolean
     ) {
         project.androidExtension.onVariants(
-            project.androidExtension.selector().withBuildType(
-                variantBuildType
-            )
+            project.androidExtension.selector().all()
         ) { variant ->
             fun setup(name: String, jniLibsSources: SourceDirectories.Layered?) {
                 checkNotNull(jniLibsSources) {
                     "Cannot find jni libs sources for variant: " +
-                        "$variant($variantBuildType / $forTest)"
+                        "$variant (forTest=$forTest)"
                 }
                 val combineTask = project.tasks.register(
                     "createJniLibsDirectoryFor".appendCapitalized(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index e2d62fc..d66a898 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -139,10 +139,11 @@
                 ?.let { metadataFile ->
                     val metadata =
                         gson.fromJson(metadataFile.readText(), ProjectStructureMetadata::class.java)
-                    metadata.sourceSets.map { sourceSet ->
+                    metadata.sourceSets.mapNotNull { sourceSet ->
+                        val sourceDir = multiplatformSourcesDir.get().asFile.resolve(sourceSet.name)
+                        if (!sourceDir.exists()) return@mapNotNull null
                         val analysisPlatform =
                             DokkaAnalysisPlatform.valueOf(sourceSet.analysisPlatform.uppercase())
-                        val sourceDir = multiplatformSourcesDir.get().asFile.resolve(sourceSet.name)
                         DokkaInputModels.SourceSet(
                             id = sourceSetIdForSourceSet(sourceSet.name),
                             displayName = sourceSet.name,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index 8fe822f..c890c2a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -93,10 +93,10 @@
 
     @get:Internal abstract val testLoader: Property<BuiltArtifactsLoader>
 
-    @get:Input abstract val testProjectPath: Property<String>
-
     @get:Input abstract val minSdk: Property<Int>
 
+    @get:Input abstract val macrobenchmark: Property<Boolean>
+
     @get:Input abstract val hasBenchmarkPlugin: Property<Boolean>
 
     @get:Input abstract val testRunner: Property<String>
@@ -192,7 +192,7 @@
                 // they run with dryRunMode to check crashes don't happen, without measurement
                 configBuilder.tag("androidx_unit_tests")
             }
-        } else if (testProjectPath.get().endsWith("macrobenchmark")) {
+        } else if (macrobenchmark.get()) {
             // macro benchmarks do not have a dryRunMode, so we don't run them in presubmit
             configBuilder.isMacrobenchmark(true)
             configBuilder.tag("macrobenchmarks")
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 9d04ef0..898da44 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -25,6 +25,7 @@
 import androidx.build.getPrivacySandboxFilesDirectory
 import androidx.build.getSupportRootFolder
 import androidx.build.hasBenchmarkPlugin
+import androidx.build.isMacrobenchmark
 import androidx.build.isPresubmitBuild
 import androidx.build.multiplatformExtension
 import com.android.build.api.artifact.Artifacts
@@ -124,10 +125,9 @@
             task.presubmit.set(isPresubmitBuild())
             task.instrumentationArgs.putAll(instrumentationRunnerArgs)
             task.minSdk.set(minSdk)
-            val hasBenchmarkPlugin = hasBenchmarkPlugin()
-            task.hasBenchmarkPlugin.set(hasBenchmarkPlugin)
+            task.hasBenchmarkPlugin.set(hasBenchmarkPlugin())
+            task.macrobenchmark.set(isMacrobenchmark())
             task.testRunner.set(testRunner)
-            task.testProjectPath.set(path)
             // Skip task if getTestSourceSetsForAndroid is empty, even if
             //  androidXExtension.deviceTests.enabled is set to true
             task.androidTestSourceCodeCollection.from(getTestSourceSetsForAndroid())
diff --git a/camera/camera-camera2-pipe-integration/OWNERS b/camera/camera-camera2-pipe-integration/OWNERS
index c445688..6df0fb2 100644
--- a/camera/camera-camera2-pipe-integration/OWNERS
+++ b/camera/camera-camera2-pipe-integration/OWNERS
@@ -2,3 +2,8 @@
 [email protected]
 [email protected]
 [email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/api/current.txt b/camera/camera-camera2-pipe-integration/api/current.txt
index 62060e3..e9d0832 100644
--- a/camera/camera-camera2-pipe-integration/api/current.txt
+++ b/camera/camera-camera2-pipe-integration/api/current.txt
@@ -55,7 +55,7 @@
     method public <ValueT> ValueT? getCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT> key);
   }
 
-  @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions> {
+  @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions?> {
     ctor public CaptureRequestOptions.Builder();
     method public androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions build();
     method public <ValueT> androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions.Builder clearCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT> key);
diff --git a/camera/camera-camera2-pipe-integration/api/restricted_current.txt b/camera/camera-camera2-pipe-integration/api/restricted_current.txt
index 62060e3..e9d0832 100644
--- a/camera/camera-camera2-pipe-integration/api/restricted_current.txt
+++ b/camera/camera-camera2-pipe-integration/api/restricted_current.txt
@@ -55,7 +55,7 @@
     method public <ValueT> ValueT? getCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT> key);
   }
 
-  @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions> {
+  @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions?> {
     ctor public CaptureRequestOptions.Builder();
     method public androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions build();
     method public <ValueT> androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions.Builder clearCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT> key);
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt
index 49e9ebe..ccc9ca7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt
@@ -22,6 +22,7 @@
 import androidx.camera.camera2.pipe.integration.compat.workaround.AutoFlashAEModeDisabler
 import androidx.camera.camera2.pipe.integration.compat.workaround.InactiveSurfaceCloser
 import androidx.camera.camera2.pipe.integration.compat.workaround.MeteringRegionCorrection
+import androidx.camera.camera2.pipe.integration.compat.workaround.UseFlashModeTorchFor3aUpdate
 import androidx.camera.camera2.pipe.integration.compat.workaround.UseTorchAsFlash
 import dagger.Module
 
@@ -31,6 +32,7 @@
         AutoFlashAEModeDisabler.Bindings::class,
         InactiveSurfaceCloser.Bindings::class,
         MeteringRegionCorrection.Bindings::class,
+        UseFlashModeTorchFor3aUpdate.Bindings::class,
         UseTorchAsFlash.Bindings::class,
     ],
 )
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
index 22cc9ce..54af34b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
@@ -98,6 +98,9 @@
         if (TextureViewIsClosedQuirk.isEnabled(cameraMetadata)) {
             quirks.add(TextureViewIsClosedQuirk())
         }
+        if (TorchFlashRequiredFor3aUpdateQuirk.isEnabled(cameraMetadata)) {
+            quirks.add(TorchFlashRequiredFor3aUpdateQuirk(cameraMetadata))
+        }
         if (YuvImageOnePixelShiftQuirk.isEnabled()) {
             quirks.add(YuvImageOnePixelShiftQuirk())
         }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchFlashRequiredFor3aUpdateQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchFlashRequiredFor3aUpdateQuirk.kt
new file mode 100644
index 0000000..aa53ceb
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchFlashRequiredFor3aUpdateQuirk.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.impl.isExternalFlashAeModeSupported
+import androidx.camera.core.impl.Quirk
+
+/**
+ * QuirkSummary
+ * - Bug Id: 294870640
+ * - Description: Quirk denoting the devices where [CaptureRequest.FLASH_MODE_TORCH] has
+ *  to be set for 3A states to be updated with good values (in some cases, AWB scanning is not
+ *  triggered at all). This results in problems like color tint or bad exposure in captured image
+ *  during captures where lighting condition changes (e.g. screen flash capture). This maybe
+ *  required even if a flash unit is not available (e.g. with front camera) and
+ *  [CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER] has been requested. If
+ *  [CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH] is supported, it can be used instead and thus
+ *  setting `FLASH_MODE_TORCH` won't be required.
+ * - Device(s): Pixel 6A, 6 PRO, 7, 7A, 7 PRO, 8, 8 PRO.
+ */
+@SuppressLint("CameraXQuirksClassDetector") // TODO: b/270421716 - enable when kotlin is supported.
+@RequiresApi(21) // TODO: b/200306659 - Remove and replace with annotation on package-info.java
+class TorchFlashRequiredFor3aUpdateQuirk(private val cameraMetadata: CameraMetadata) : Quirk {
+    /**
+     * Returns whether [CaptureRequest.FLASH_MODE_TORCH] is required to be set.
+     *
+     * This will check if the [CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH] is supported, which
+     * is more recommended than using a quirk like using `FLASH_MODE_TORCH`.
+     */
+    fun isFlashModeTorchRequired() = !cameraMetadata.isExternalFlashAeModeSupported()
+
+    companion object {
+        private val AFFECTED_PIXEL_MODELS: List<String> = mutableListOf(
+            "PIXEL 6A",
+            "PIXEL 6 PRO",
+            "PIXEL 7",
+            "PIXEL 7A",
+            "PIXEL 7 PRO",
+            "PIXEL 8",
+            "PIXEL 8 PRO"
+        )
+
+        fun isEnabled(cameraMetadata: CameraMetadata) = isAffectedModel(cameraMetadata)
+
+        private fun isAffectedModel(cameraMetadata: CameraMetadata) =
+            isAffectedPixelModel() && cameraMetadata.isFrontCamera
+
+        private fun isAffectedPixelModel(): Boolean {
+            AFFECTED_PIXEL_MODELS.forEach { model ->
+                if (Build.MODEL.uppercase() == model) {
+                    return true
+                }
+            }
+            return false
+        }
+
+        private val CameraMetadata.isFrontCamera: Boolean
+            get() = this[CameraCharacteristics.LENS_FACING] == LENS_FACING_FRONT
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/UseFlashModeTorchFor3aUpdate.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/UseFlashModeTorchFor3aUpdate.kt
new file mode 100644
index 0000000..e3a8c7a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/UseFlashModeTorchFor3aUpdate.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(21) // TODO: b/200306659 - Remove and replace with annotation on package-info.java
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.hardware.camera2.CaptureRequest
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.TorchFlashRequiredFor3aUpdateQuirk
+import dagger.Module
+import dagger.Provides
+
+/**
+ * Workaround to use [CaptureRequest.FLASH_MODE_TORCH] for 3A operation.
+ *
+ * @see TorchFlashRequiredFor3aUpdateQuirk
+ */
+interface UseFlashModeTorchFor3aUpdate {
+    fun shouldUseFlashModeTorch(): Boolean
+
+    @Module
+    abstract class Bindings {
+        companion object {
+            @Provides
+            fun provideUseFlashModeTorchFor3aUpdate(
+                cameraQuirks: CameraQuirks
+            ): UseFlashModeTorchFor3aUpdate =
+                if (cameraQuirks.quirks.contains(TorchFlashRequiredFor3aUpdateQuirk::class.java))
+                    UseFlashModeTorchFor3aUpdateImpl
+                else
+                    NotUseFlashModeTorchFor3aUpdate
+        }
+    }
+}
+
+object UseFlashModeTorchFor3aUpdateImpl : UseFlashModeTorchFor3aUpdate {
+    /** Returns true for torch should be used as flash. */
+    override fun shouldUseFlashModeTorch() = true
+}
+
+object NotUseFlashModeTorchFor3aUpdate : UseFlashModeTorchFor3aUpdate {
+    override fun shouldUseFlashModeTorch() = false
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraMetadataIntegration.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraMetadataIntegration.kt
new file mode 100644
index 0000000..080008e
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraMetadataIntegration.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(21) // TODO: b/200306659 - Remove and replace with annotation on package-info.java
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+
+/**
+ * Contains the CameraX-specific logic for [CameraMetadata].
+ */
+
+val CameraMetadata.availableAfModes
+        get() = getOrDefault(
+            CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES,
+            intArrayOf(CaptureRequest.CONTROL_AF_MODE_OFF)
+        ).asList()
+
+val CameraMetadata.availableAeModes
+    get() = getOrDefault(
+        CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
+        intArrayOf(CaptureRequest.CONTROL_AE_MODE_OFF)
+    ).asList()
+
+val CameraMetadata.availableAwbModes
+    get() = getOrDefault(
+        CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES,
+        intArrayOf(CaptureRequest.CONTROL_AWB_MODE_OFF)
+    ).asList()
+
+/**
+ * If preferredMode not available, priority is CONTINUOUS_PICTURE > AUTO > OFF
+ */
+fun CameraMetadata.getSupportedAfMode(preferredMode: Int) = when {
+    availableAfModes.contains(preferredMode) -> {
+        preferredMode
+    }
+
+    availableAfModes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) -> {
+        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+    }
+
+    availableAfModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO) -> {
+        CaptureRequest.CONTROL_AF_MODE_AUTO
+    }
+
+    else -> {
+        CaptureRequest.CONTROL_AF_MODE_OFF
+    }
+}
+
+/**
+ * If preferredMode not available, priority is AE_ON > AE_OFF
+ */
+fun CameraMetadata.getSupportedAeMode(preferredMode: Int) = when {
+    availableAeModes.contains(preferredMode) -> {
+        preferredMode
+    }
+
+    availableAeModes.contains(CaptureRequest.CONTROL_AE_MODE_ON) -> {
+        CaptureRequest.CONTROL_AE_MODE_ON
+    }
+
+    else -> {
+        CaptureRequest.CONTROL_AE_MODE_OFF
+    }
+}
+
+private fun CameraMetadata.isAeModeSupported(aeMode: Int) = getSupportedAeMode(aeMode) == aeMode
+
+/** Returns whether [CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH] is supported. */
+fun CameraMetadata.isExternalFlashAeModeSupported() =
+    Build.VERSION.SDK_INT >= 28 &&
+        isAeModeSupported(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH)
+
+/**
+ * If preferredMode not available, priority is AWB_AUTO > AWB_OFF
+ */
+fun CameraMetadata.getSupportedAwbMode(preferredMode: Int) = when {
+    availableAwbModes.contains(preferredMode) -> {
+        preferredMode
+    }
+
+    availableAwbModes.contains(CaptureRequest.CONTROL_AWB_MODE_AUTO) -> {
+        CaptureRequest.CONTROL_AWB_MODE_AUTO
+    }
+
+    else -> {
+        CaptureRequest.CONTROL_AWB_MODE_OFF
+    }
+}
+
+fun <T> CameraMetadata?.getOrDefault(
+    key: CameraCharacteristics.Key<T>,
+    default: T
+) = this?.getOrDefault(key, default) ?: default
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
index 125f776..3eb4b54 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
@@ -21,6 +21,7 @@
 import androidx.camera.camera2.pipe.core.Log.warn
 import androidx.camera.camera2.pipe.integration.adapter.awaitUntil
 import androidx.camera.camera2.pipe.integration.adapter.propagateTo
+import androidx.camera.camera2.pipe.integration.compat.workaround.UseFlashModeTorchFor3aUpdate
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.core.CameraControl
 import androidx.camera.core.ImageCapture
@@ -48,8 +49,11 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @CameraScope
 class FlashControl @Inject constructor(
+    private val cameraProperties: CameraProperties,
     private val state3AControl: State3AControl,
     private val threads: UseCaseThreads,
+    private val torchControl: TorchControl,
+    private val useFlashModeTorchFor3aUpdate: UseFlashModeTorchFor3aUpdate,
 ) : UseCaseCameraControl {
     private var _useCaseCamera: UseCaseCamera? = null
     override var useCaseCamera: UseCaseCamera?
@@ -148,32 +152,17 @@
         val pendingTasks = mutableListOf<Deferred<Unit>>()
 
         // Invoke ScreenFlash#apply and wait later for its listener to be completed
-        applyScreenFlash(
-            TimeUnit.SECONDS.toMillis(ImageCapture.SCREEN_FLASH_UI_APPLY_TIMEOUT_SECONDS)
-        ).let {
-            pendingTasks.add(it)
-        }
+        pendingTasks.add(
+            applyScreenFlash(
+                TimeUnit.SECONDS.toMillis(ImageCapture.SCREEN_FLASH_UI_APPLY_TIMEOUT_SECONDS)
+            )
+        )
 
-        // Enable external flash AE mode if possible
-        val isExternalFlashAeModeSupported = state3AControl.isExternalFlashAeModeSupported()
-        debug {
-            "startScreenFlashCaptureTasks: isExternalFlashAeModeSupported = " +
-                "$isExternalFlashAeModeSupported"
-        }
-        if (isExternalFlashAeModeSupported) {
-            state3AControl.tryExternalFlashAeMode = true
-            state3AControl.updateSignal?.let {
-                debug {
-                    "startScreenFlashCaptureTasks: need to wait for state3AControl.updateSignal"
-                }
-                pendingTasks.add(it)
-                it.invokeOnCompletion {
-                    debug { "startScreenFlashCaptureTasks: state3AControl.updateSignal completed" }
-                }
-            }
-        }
+        // Try to set external flash AE mode if possible
+        setExternalFlashAeModeAsync()?.let { pendingTasks.add(it) }
 
-        // TODO: b/326170400 - Enable torch mode if TorchFlashRequiredFor3aUpdateQuirk added
+        // Set FLASH_MODE_TORCH for quirks
+        setTorchForScreenFlash()?.let { pendingTasks.add(it) }
 
         pendingTasks.awaitAll()
     }
@@ -215,18 +204,77 @@
         }
     }
 
+    /**
+     * Tries to set external flash AE mode if possible.
+     *
+     * @return A [Deferred] that reports the completion of the operation, `null` if not supported.
+     */
+    private fun setExternalFlashAeModeAsync(): Deferred<Unit>? {
+        val isExternalFlashAeModeSupported =
+            cameraProperties.metadata.isExternalFlashAeModeSupported()
+        debug {
+            "setExternalFlashAeModeAsync: isExternalFlashAeModeSupported = " +
+                "$isExternalFlashAeModeSupported"
+        }
+
+        if (!isExternalFlashAeModeSupported) {
+            return null
+        }
+
+        state3AControl.tryExternalFlashAeMode = true
+        return state3AControl.updateSignal?.also {
+            debug {
+                "setExternalFlashAeModeAsync: need to wait for state3AControl.updateSignal"
+            }
+            it.invokeOnCompletion {
+                debug { "setExternalFlashAeModeAsync: state3AControl.updateSignal completed" }
+            }
+        }
+    }
+
+    /**
+     * Enables the torch mode for screen flash capture when required.
+     *
+     * Since this is required due to a device quirk despite lacking physical flash unit, the
+     * `ignoreFlashUnitAvailability` parameter is set to `true` while invoking
+     * [TorchControl.setTorchAsync].
+     *
+     * @return A [Deferred] that reports the completion of the operation, `null` if not required.
+     */
+    private fun setTorchForScreenFlash(): Deferred<Unit>? {
+        val shouldUseFlashModeTorch = useFlashModeTorchFor3aUpdate.shouldUseFlashModeTorch()
+        debug {
+            "setTorchIfRequired: shouldUseFlashModeTorch = $shouldUseFlashModeTorch"
+        }
+
+        if (!shouldUseFlashModeTorch) {
+            return null
+        }
+
+        return torchControl.setTorchAsync(torch = true, ignoreFlashUnitAvailability = true).also {
+            debug {
+                "setTorchIfRequired: need to wait for torch control to be completed"
+            }
+            it.invokeOnCompletion {
+                debug { "setTorchIfRequired: torch control completed" }
+            }
+        }
+    }
+
     suspend fun stopScreenFlashCaptureTasks() {
         withContext(Dispatchers.Main) {
             screenFlash?.clear()
             debug { "screenFlashPostCapture: ScreenFlash.clear() invoked" }
         }
 
-        if (state3AControl.isExternalFlashAeModeSupported()) {
+        if (cameraProperties.metadata.isExternalFlashAeModeSupported()) {
             // Disable external flash AE mode, ok to complete whenever
             state3AControl.tryExternalFlashAeMode = false
         }
 
-        // TODO: b/326170400 - Disable torch mode if TorchFlashRequiredFor3aUpdateQuirk added
+        if (useFlashModeTorchFor3aUpdate.shouldUseFlashModeTorch()) {
+            torchControl.setTorchAsync(torch = false, ignoreFlashUnitAvailability = true)
+        }
     }
 
     @Module
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt
index db8129e..861f79f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt
@@ -195,8 +195,9 @@
         val processorSessionConfig = synchronized(lock) {
             if (isClosed()) return@launch configure(null)
             try {
-                DeferrableSurfaces.incrementAll(deferrableSurfaces)
-                postviewDeferrableSurface?.incrementUseCount()
+                val surfacesToIncrement = ArrayList(deferrableSurfaces)
+                postviewDeferrableSurface?.let { surfacesToIncrement.add(it) }
+                DeferrableSurfaces.incrementAll(surfacesToIncrement)
             } catch (exception: DeferrableSurface.SurfaceClosedException) {
                 sessionConfigAdapter.reportSurfaceInvalid(exception.deferrableSurface)
                 return@launch configure(null)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
index fb05144..99f069e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
@@ -16,10 +16,8 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
-import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CaptureRequest
-import android.os.Build
 import android.util.Range
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
@@ -47,7 +45,7 @@
 class State3AControl @Inject constructor(
     val cameraProperties: CameraProperties,
     private val aeModeDisabler: AutoFlashAEModeDisabler,
-    private val aeFpsRange: AeFpsRange
+    private val aeFpsRange: AeFpsRange,
 ) : UseCaseCameraControl, UseCaseCamera.RunningUseCasesChangeListener {
     private var _useCaseCamera: UseCaseCamera? = null
     override var useCaseCamera: UseCaseCamera?
@@ -74,19 +72,6 @@
         }
     }
 
-    private val afModes = cameraProperties.metadata.getOrDefault(
-        CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES,
-        intArrayOf(CaptureRequest.CONTROL_AF_MODE_OFF)
-    ).asList()
-    private val aeModes = cameraProperties.metadata.getOrDefault(
-        CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
-        intArrayOf(CaptureRequest.CONTROL_AE_MODE_OFF)
-    ).asList()
-    private val awbModes = cameraProperties.metadata.getOrDefault(
-        CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES,
-        intArrayOf(CaptureRequest.CONTROL_AWB_MODE_OFF)
-    ).asList()
-
     private val lock = Any()
 
     @GuardedBy("lock")
@@ -134,7 +119,7 @@
 
         // Overwrite AE mode to ON_EXTERNAL_FLASH only if required and explicitly supported
         if (tryExternalFlashAeMode) {
-            val isSupported = isExternalFlashAeModeSupported()
+            val isSupported = cameraProperties.metadata.isExternalFlashAeModeSupported()
             debug { "State3AControl.invalidate: trying external flash AE mode" +
                 ", supported = $isSupported" }
             if (isSupported) {
@@ -154,10 +139,16 @@
         val preferAfMode = preferredFocusMode ?: getDefaultAfMode()
 
         val parameters: MutableMap<CaptureRequest.Key<*>, Any> = mutableMapOf(
-            CaptureRequest.CONTROL_AE_MODE to getSupportedAeMode(preferAeMode),
-            CaptureRequest.CONTROL_AF_MODE to getSupportedAfMode(preferAfMode),
-            CaptureRequest.CONTROL_AWB_MODE to getSupportedAwbMode(
-                CaptureRequest.CONTROL_AWB_MODE_AUTO))
+            CaptureRequest.CONTROL_AE_MODE to cameraProperties.metadata.getSupportedAeMode(
+                preferAeMode
+            ),
+            CaptureRequest.CONTROL_AF_MODE to cameraProperties.metadata.getSupportedAfMode(
+                preferAfMode
+            ),
+            CaptureRequest.CONTROL_AWB_MODE to cameraProperties.metadata.getSupportedAwbMode(
+                CaptureRequest.CONTROL_AWB_MODE_AUTO
+            )
+        )
 
         preferredAeFpsRange?.let {
             parameters[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] = it
@@ -188,67 +179,6 @@
         else -> CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
     }
 
-    /**
-     * If preferredMode not available, priority is CONTINUOUS_PICTURE > AUTO > OFF
-     */
-    private fun getSupportedAfMode(preferredMode: Int) = when {
-        afModes.contains(preferredMode) -> {
-            preferredMode
-        }
-
-        afModes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) -> {
-            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
-        }
-
-        afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO) -> {
-            CaptureRequest.CONTROL_AF_MODE_AUTO
-        }
-
-        else -> {
-            CaptureRequest.CONTROL_AF_MODE_OFF
-        }
-    }
-
-    /**
-     * If preferredMode not available, priority is AE_ON > AE_OFF
-     */
-    private fun getSupportedAeMode(preferredMode: Int) = when {
-        aeModes.contains(preferredMode) -> {
-            preferredMode
-        }
-
-        aeModes.contains(CaptureRequest.CONTROL_AE_MODE_ON) -> {
-            CaptureRequest.CONTROL_AE_MODE_ON
-        }
-
-        else -> {
-            CaptureRequest.CONTROL_AE_MODE_OFF
-        }
-    }
-
-    private fun isAeModeSupported(aeMode: Int) = getSupportedAeMode(aeMode) == aeMode
-
-    fun isExternalFlashAeModeSupported() =
-        Build.VERSION.SDK_INT >= 28 &&
-            isAeModeSupported(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH)
-
-    /**
-     * If preferredMode not available, priority is AWB_AUTO > AWB_OFF
-     */
-    private fun getSupportedAwbMode(preferredMode: Int) = when {
-        awbModes.contains(preferredMode) -> {
-            preferredMode
-        }
-
-        awbModes.contains(CaptureRequest.CONTROL_AWB_MODE_AUTO) -> {
-            CaptureRequest.CONTROL_AWB_MODE_AUTO
-        }
-
-        else -> {
-            CaptureRequest.CONTROL_AWB_MODE_OFF
-        }
-    }
-
     private fun Collection<UseCase>.updateTemplate() {
         SessionConfigAdapter(this).getValidSessionConfigOrNull()?.let {
             val templateType = it.repeatingCaptureConfig.templateType
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
index 683ef5f..1699de3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
@@ -76,10 +76,22 @@
 
     private var _updateSignal: CompletableDeferred<Unit>? = null
 
-    fun setTorchAsync(torch: Boolean, cancelPreviousTask: Boolean = true): Deferred<Unit> {
+    /**
+     * Turn the torch on or off.
+     *
+     * @param torch Whether the torch should be on or off.
+     * @param cancelPreviousTask Whether to cancel the previous task if it's running.
+     * @param ignoreFlashUnitAvailability Whether to ignore the flash unit availability. When true,
+     *      torch mode setting will be attempted even if a physical flash unit is not available.
+     */
+    fun setTorchAsync(
+        torch: Boolean,
+        cancelPreviousTask: Boolean = true,
+        ignoreFlashUnitAvailability: Boolean = false
+    ): Deferred<Unit> {
         val signal = CompletableDeferred<Unit>()
 
-        if (!hasFlashUnit) {
+        if (!ignoreFlashUnitAvailability && !hasFlashUnit) {
             return signal.createFailureResult(IllegalStateException("No flash unit"))
         }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchFlashRequiredFor3aUpdateQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchFlashRequiredFor3aUpdateQuirkTest.kt
new file mode 100644
index 0000000..35bd790
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchFlashRequiredFor3aUpdateQuirkTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.Quirks
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowBuild
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = 21)
+class TorchFlashRequiredFor3aUpdateQuirkTest(
+    private val model: String,
+    private val lensFacing: Int,
+    private val externalFlashAeModeSupported: Boolean,
+    private val enabled: Boolean
+) {
+    companion object {
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(
+            name = "Model: {0}, lens facing: {1}, external ae mode: {2}, enabled: {3}"
+        )
+        fun data() = listOf(
+            arrayOf("Pixel 3a", CameraCharacteristics.LENS_FACING_FRONT, false, false),
+            arrayOf("Pixel 4", CameraCharacteristics.LENS_FACING_FRONT, true, false),
+            arrayOf("Pixel 6", CameraCharacteristics.LENS_FACING_FRONT, false, false),
+            arrayOf("Pixel 6A", CameraCharacteristics.LENS_FACING_BACK, false, false),
+            arrayOf("Pixel 6A", CameraCharacteristics.LENS_FACING_FRONT, false, true),
+            arrayOf("Pixel 7 pro", CameraCharacteristics.LENS_FACING_FRONT, false, true),
+            arrayOf("Pixel 8", CameraCharacteristics.LENS_FACING_FRONT, false, true),
+            arrayOf("SM-A320FL", CameraCharacteristics.LENS_FACING_FRONT, false, false),
+        )
+    }
+
+    private fun getCameraQuirks(
+        lensFacing: Int,
+        externalFlashAeModeSupported: Boolean,
+    ): Quirks {
+        val characteristicsMap = mutableMapOf<CameraCharacteristics.Key<*>, Any?>().apply {
+            this[CameraCharacteristics.LENS_FACING] = lensFacing
+
+            this[CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES] =
+                if (externalFlashAeModeSupported) {
+                    intArrayOf(CameraMetadata.CONTROL_AE_MODE_ON_EXTERNAL_FLASH)
+                } else intArrayOf(
+                    CameraMetadata.CONTROL_AE_MODE_ON
+                )
+        }.toMap()
+
+        val cameraCharacteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+        val shadowCharacteristics =
+            Shadow.extract<ShadowCameraCharacteristics>(cameraCharacteristics)
+        characteristicsMap.forEach { entry ->
+            shadowCharacteristics.set(entry.key, entry.value)
+        }
+
+        val cameraMetadata = FakeCameraMetadata(characteristicsMap)
+
+        return CameraQuirks(
+            cameraMetadata,
+            StreamConfigurationMapCompat(
+                StreamConfigurationMapBuilder.newBuilder().build(),
+                OutputSizesCorrector(
+                    cameraMetadata,
+                    StreamConfigurationMapBuilder.newBuilder().build()
+                )
+            ),
+        ).quirks
+    }
+
+    @Test
+    fun canEnableQuirkCorrectly() {
+        // Arrange
+        ShadowBuild.setModel(model)
+        val cameraQuirks = getCameraQuirks(lensFacing, externalFlashAeModeSupported)
+
+        // Act
+        val isFlashModeTorchRequired =
+            cameraQuirks.get(TorchFlashRequiredFor3aUpdateQuirk::class.java)
+                ?.isFlashModeTorchRequired() ?: false
+
+        // Verify
+        Truth.assertThat(isFlashModeTorchRequired).isEqualTo(enabled)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CameraMetadataIntegrationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CameraMetadataIntegrationTest.kt
new file mode 100644
index 0000000..2dd94aa
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CameraMetadataIntegrationTest.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata
+import android.os.Build
+import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@DoNotInstrument
+class CameraMetadataIntegrationTest {
+    private lateinit var cameraMetadata: androidx.camera.camera2.pipe.CameraMetadata
+
+    private fun initCameraMetadata(
+        cameraCharacteristics: Map<CameraCharacteristics.Key<*>, Any?> = emptyMap()
+    ) {
+        cameraMetadata = FakeCameraMetadata(cameraCharacteristics)
+    }
+
+    @Before
+    fun setUp() {
+        initCameraMetadata()
+    }
+
+    @Test
+    fun getSupportedAeMode_returnsPreferredMode_whenSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
+                CameraMetadata.CONTROL_AE_MODE_ON,
+                CameraMetadata.CONTROL_AE_MODE_OFF,
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAeMode(CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
+        ).isEqualTo(CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
+    }
+
+    @Test
+    fun getSupportedAeMode_returnsAeModeOnIfSupported_whenPreferredModeNotSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AE_MODE_ON,
+                CameraMetadata.CONTROL_AE_MODE_OFF,
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAeMode(CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
+        ).isEqualTo(CameraMetadata.CONTROL_AE_MODE_ON)
+    }
+
+    @Test
+    fun getSupportedAeMode_returnsAeModeOff_whenPreferredModeAndAeModeOnNotSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AE_MODE_OFF,
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAeMode(CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
+        ).isEqualTo(CameraMetadata.CONTROL_AE_MODE_OFF)
+    }
+
+    @Test
+    @Config(maxSdk = 27)
+    fun isExternalFlashAeModeSupported_returnsFalseWhenBelowApiLevel28() {
+        assertThat(cameraMetadata.isExternalFlashAeModeSupported()).isFalse()
+    }
+
+    @Test
+    @Config(minSdk = 28)
+    fun isExternalFlashAeModeSupported_returnsFalseWhenNotSupported() {
+        assertThat(cameraMetadata.isExternalFlashAeModeSupported()).isFalse()
+    }
+
+    @Test
+    @Config(minSdk = 28)
+    fun isExternalFlashAeModeSupported_returnsTrueWhenSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AE_MODE_ON_EXTERNAL_FLASH,
+            )
+        ))
+
+        assertThat(cameraMetadata.isExternalFlashAeModeSupported()).isTrue()
+    }
+
+    @Test
+    fun getSupportedAfMode_returnsPreferredMode_whenSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO,
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAfMode(CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
+        ).isEqualTo(CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
+    }
+
+    @Test
+    fun getSupportedAfMode_returnsContinuousPictureIfSupported_whenPreferredModeNotSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+                CameraMetadata.CONTROL_AF_MODE_AUTO,
+                CameraMetadata.CONTROL_AF_MODE_OFF
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAfMode(CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
+        ).isEqualTo(CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
+    }
+
+    @Test
+    fun getSupportedAfMode_returnsAfModeAutoIfSupported_whenNoPreferredModeAndContinuousPicture() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AF_MODE_AUTO,
+                CameraMetadata.CONTROL_AF_MODE_OFF
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAfMode(CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
+        ).isEqualTo(CameraMetadata.CONTROL_AF_MODE_AUTO)
+    }
+
+    @Test
+    fun getSupportedAfMode_returnsAfModeOff_whenNoPreferredModeAndContinuousPictureAndAfModeAuto() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AF_MODE_OFF
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAfMode(CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
+        ).isEqualTo(CameraMetadata.CONTROL_AF_MODE_OFF)
+    }
+
+    @Test
+    fun getSupportedAwbMode_returnsPreferredMode_whenSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT,
+                CameraMetadata.CONTROL_AWB_MODE_AUTO,
+                CameraMetadata.CONTROL_AWB_MODE_OFF
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAwbMode(CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT)
+        ).isEqualTo(CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT)
+    }
+
+    @Test
+    fun getSupportedAwbMode_returnsAwbModeAutoIfSupported_whenPreferredModeNotSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AWB_MODE_AUTO,
+                CameraMetadata.CONTROL_AWB_MODE_OFF
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAwbMode(CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT)
+        ).isEqualTo(CameraMetadata.CONTROL_AWB_MODE_AUTO)
+    }
+
+    @Test
+    fun getSupportedAwbMode_returnsAwbModeOff_whenPreferredModeAndAwbModeAutoNotSupported() {
+        initCameraMetadata(mapOf(
+            CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES to intArrayOf(
+                CameraMetadata.CONTROL_AWB_MODE_OFF
+            )
+        ))
+
+        assertThat(
+            cameraMetadata.getSupportedAwbMode(CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT)
+        ).isEqualTo(CameraMetadata.CONTROL_AWB_MODE_OFF)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index d2772f5..4ad90e72 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -46,6 +46,7 @@
 import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
 import androidx.camera.camera2.pipe.integration.compat.workaround.CapturePipelineTorchCorrection
 import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
+import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseFlashModeTorchFor3aUpdate
 import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseTorchAsFlash
 import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
 import androidx.camera.camera2.pipe.integration.compat.workaround.UseTorchAsFlashImpl
@@ -272,18 +273,11 @@
                         )
                     )
                 )
-            )
+            ),
         ).apply {
             useCaseCamera = fakeUseCaseCamera
         }
 
-        flashControl = FlashControl(
-            state3AControl = state3AControl,
-            threads = fakeUseCaseThreads,
-        ).apply {
-            setScreenFlash([email protected])
-        }
-
         torchControl = TorchControl(
             fakeCameraProperties,
             state3AControl,
@@ -298,6 +292,16 @@
             fakeRequestControl.torchUpdateEventList.clear()
         }
 
+        flashControl = FlashControl(
+            cameraProperties = fakeCameraProperties,
+            state3AControl = state3AControl,
+            threads = fakeUseCaseThreads,
+            torchControl = torchControl,
+            useFlashModeTorchFor3aUpdate = NotUseFlashModeTorchFor3aUpdate,
+        ).apply {
+            setScreenFlash([email protected])
+        }
+
         fakeUseCaseCameraState = UseCaseCameraState(
             fakeUseCaseGraphConfig,
             fakeUseCaseThreads,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
index 36838d5..d7b27cc 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
@@ -25,13 +25,16 @@
 import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
 import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
 import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
+import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseFlashModeTorchFor3aUpdate
 import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.integration.compat.workaround.UseFlashModeTorchFor3aUpdateImpl
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.camera.core.CameraControl
 import androidx.camera.core.ImageCapture
+import androidx.camera.core.TorchState
 import androidx.camera.testing.impl.mocks.MockScreenFlash
 import androidx.testutils.MainDispatcherRule
 import androidx.testutils.assertThrows
@@ -94,6 +97,7 @@
         )
     )
     private lateinit var state3AControl: State3AControl
+    private lateinit var torchControl: TorchControl
     private lateinit var flashControl: FlashControl
 
     private val screenFlash = MockScreenFlash()
@@ -103,7 +107,10 @@
         createFlashControl()
     }
 
-    private fun createFlashControl(addExternalFlashAeMode: Boolean = false) {
+    private fun createFlashControl(
+        addExternalFlashAeMode: Boolean = false,
+        useFlashModeTorch: Boolean = false,
+    ) {
         val aeAvailableModes = mutableListOf(
             CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
             CaptureRequest.CONTROL_AE_MODE_ON,
@@ -120,18 +127,34 @@
                 CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES to aeAvailableModes.toIntArray()
             )
         )
+        val cameraProperties = FakeCameraProperties(metadata)
 
         state3AControl = State3AControl(
-            FakeCameraProperties(metadata),
+            cameraProperties,
             NoOpAutoFlashAEModeDisabler,
-            aeFpsRange
+            aeFpsRange,
+        ).apply {
+            useCaseCamera = fakeUseCaseCamera
+        }
+
+        torchControl = TorchControl(
+            cameraProperties,
+            state3AControl,
+            fakeUseCaseThreads
         ).apply {
             useCaseCamera = fakeUseCaseCamera
         }
 
         flashControl = FlashControl(
+            cameraProperties = cameraProperties,
             state3AControl = state3AControl,
             threads = fakeUseCaseThreads,
+            torchControl = torchControl,
+            useFlashModeTorchFor3aUpdate = if (useFlashModeTorch) {
+                UseFlashModeTorchFor3aUpdateImpl
+            } else {
+                NotUseFlashModeTorchFor3aUpdate
+            },
         )
         flashControl.useCaseCamera = fakeUseCaseCamera
         flashControl.setScreenFlash(screenFlash)
@@ -143,14 +166,21 @@
         val fakeCameraProperties = FakeCameraProperties()
 
         val flashControl = FlashControl(
+            fakeCameraProperties,
             State3AControl(
                 fakeCameraProperties,
                 NoOpAutoFlashAEModeDisabler,
-                aeFpsRange
+                aeFpsRange,
             ).apply {
                 useCaseCamera = fakeUseCaseCamera
             },
             fakeUseCaseThreads,
+            TorchControl(
+                fakeCameraProperties,
+                state3AControl,
+                fakeUseCaseThreads
+            ),
+            NotUseFlashModeTorchFor3aUpdate
         )
 
         assertThrows<CameraControl.OperationCanceledException> {
@@ -370,6 +400,44 @@
     }
 
     @Test
+    fun torchNotEnabledAtScreenFlashCapture_whenNotRequired() = runTest {
+        createFlashControl(addExternalFlashAeMode = false, useFlashModeTorch = false)
+
+        flashControl.startScreenFlashCaptureTasks()
+
+        assertThat(torchControl.torchStateLiveData.value).isEqualTo(TorchState.OFF)
+    }
+
+    @Test
+    fun torchEnabledAtScreenFlashCapture_whenRequired() = runTest {
+        createFlashControl(addExternalFlashAeMode = false, useFlashModeTorch = true)
+
+        flashControl.startScreenFlashCaptureTasks()
+
+        assertThat(torchControl.torchStateLiveData.value).isEqualTo(TorchState.ON)
+    }
+
+    @Test
+    fun torchEnabled_whenScreenFlashCaptureApplyNotCompleted() = runTest {
+        createFlashControl(addExternalFlashAeMode = false, useFlashModeTorch = true)
+        screenFlash.setApplyCompletedInstantly(false)
+
+        flashControl.startScreenFlashCaptureTasks()
+
+        assertThat(torchControl.torchStateLiveData.value).isEqualTo(TorchState.ON)
+    }
+
+    @Test
+    fun torchDisabledAtScreenFlashCaptureStop_whenRequired() = runTest {
+        createFlashControl(addExternalFlashAeMode = false, useFlashModeTorch = true)
+        flashControl.startScreenFlashCaptureTasks()
+
+        flashControl.stopScreenFlashCaptureTasks()
+
+        assertThat(torchControl.torchStateLiveData.value).isEqualTo(TorchState.OFF)
+    }
+
+    @Test
     fun screenFlashClearInvokedInMainThread_whenStopped() = runTest {
         withContext(Dispatchers.IO) { // ensures initial call is not from main thread
             flashControl.stopScreenFlashCaptureTasks()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
index 7e05158..e708509 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
@@ -22,6 +22,7 @@
 import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseFlashModeTorchFor3aUpdate
 import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseTorchAsFlash
 import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraph
@@ -91,9 +92,18 @@
 
     private lateinit var fakeUseCaseCamera: UseCaseCamera
 
+    private val torchControl = TorchControl(
+        fakeCameraProperties,
+        fakeState3AControl,
+        fakeUseCaseThreads
+    )
+
     private val flashControl = FlashControl(
+        fakeCameraProperties,
         fakeState3AControl,
         fakeUseCaseThreads,
+        torchControl,
+        NotUseFlashModeTorchFor3aUpdate,
     )
 
     private val stillCaptureRequestControl = StillCaptureRequestControl(
@@ -438,24 +448,28 @@
             threads = fakeUseCaseThreads,
             sessionProcessorManager = null,
         )
+        val torchControl = TorchControl(
+            fakeCameraProperties,
+            fakeState3AControl,
+            fakeUseCaseThreads
+        )
         requestControl = UseCaseCameraRequestControlImpl(
             capturePipeline = CapturePipelineImpl(
                 configAdapter = fakeConfigAdapter,
                 cameraProperties = fakeCameraProperties,
                 requestListener = ComboRequestListener(),
                 threads = fakeUseCaseThreads,
-                torchControl = TorchControl(
-                    fakeCameraProperties,
-                    fakeState3AControl,
-                    fakeUseCaseThreads
-                ),
+                torchControl = torchControl,
                 useCaseGraphConfig = fakeUseCaseGraphConfig,
                 useCaseCameraState = fakeUseCaseCameraState,
                 useTorchAsFlash = NotUseTorchAsFlash,
                 sessionProcessorManager = null,
                 flashControl = FlashControl(
+                    cameraProperties = fakeCameraProperties,
                     state3AControl = fakeState3AControl,
                     threads = fakeUseCaseThreads,
+                    torchControl = torchControl,
+                    useFlashModeTorchFor3aUpdate = NotUseFlashModeTorchFor3aUpdate,
                 ),
             ),
             state = fakeUseCaseCameraState,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
index e5d846e..f4cdd2d 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
@@ -109,7 +109,7 @@
             State3AControl(
                 fakeCameraProperties,
                 NoOpAutoFlashAEModeDisabler,
-                aeFpsRange
+                aeFpsRange,
             ).apply {
                 useCaseCamera = fakeUseCaseCamera
             },
@@ -130,7 +130,7 @@
                 State3AControl(
                     fakeCameraProperties,
                     NoOpAutoFlashAEModeDisabler,
-                    aeFpsRange
+                    aeFpsRange,
                 ).apply {
                     useCaseCamera = fakeUseCaseCamera
                 },
@@ -151,7 +151,7 @@
             State3AControl(
                 fakeCameraProperties,
                 NoOpAutoFlashAEModeDisabler,
-                aeFpsRange
+                aeFpsRange,
             ).apply {
 
                 useCaseCamera = fakeUseCaseCamera
@@ -175,7 +175,7 @@
                 State3AControl(
                     fakeCameraProperties,
                     NoOpAutoFlashAEModeDisabler,
-                    aeFpsRange
+                    aeFpsRange,
                 ).apply {
                     useCaseCamera = fakeUseCaseCamera
                 },
@@ -198,6 +198,30 @@
     }
 
     @Test
+    fun enableTorch_torchStateOn_whenNoFlashUnit_butFlashUnitAvailabilityIsIgnored() = runBlocking {
+        val fakeUseCaseCamera = FakeUseCaseCamera()
+        val fakeCameraProperties = FakeCameraProperties()
+
+        val torchControl = TorchControl(
+            fakeCameraProperties,
+            State3AControl(
+                fakeCameraProperties,
+                NoOpAutoFlashAEModeDisabler,
+                aeFpsRange,
+            ).apply {
+                useCaseCamera = fakeUseCaseCamera
+            },
+            fakeUseCaseThreads,
+        ).also {
+            it.useCaseCamera = fakeUseCaseCamera
+            it.setTorchAsync(torch = true, ignoreFlashUnitAvailability = true)
+        }
+
+        // LiveData is updated synchronously. Don't need to wait for the result of the setTorchAsync
+        Truth.assertThat(torchControl.torchStateLiveData.value).isEqualTo(TorchState.ON)
+    }
+
+    @Test
     fun disableTorch_TorchStateOff() {
         torchControl.setTorchAsync(true)
         // LiveData is updated synchronously. Don't need to wait for the result of the setTorchAsync
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
index 50cde53..ebb8d55 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
@@ -22,6 +22,7 @@
 import androidx.camera.camera2.pipe.GraphState
 import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
@@ -72,6 +73,14 @@
         setSurfaceResults[stream] = surface
     }
 
+    override fun getAudioRestriction(): AudioRestrictionMode? {
+        throw NotImplementedError("Not used in testing")
+    }
+
+    override fun setAudioRestriction(mode: AudioRestrictionMode) {
+        throw NotImplementedError("Not used in testing")
+    }
+
     override fun start() {
         throw NotImplementedError("Not used in testing")
     }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index 4691ba1..427d14d 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -121,7 +121,7 @@
         val state3AControl = State3AControl(
             cameraProperties,
             NoOpAutoFlashAEModeDisabler,
-            AeFpsRange(fakeCameraQuirks)
+            AeFpsRange(fakeCameraQuirks),
         ).apply {
             useCaseCamera = fakeUseCaseCamera
         }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeState3AControlCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeState3AControlCreator.kt
index 3b30f5d..055e7f4 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeState3AControlCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeState3AControlCreator.kt
@@ -46,7 +46,7 @@
                     )
                 )
             )
-        )
+        ),
     ).apply {
         this.useCaseCamera = useCaseCamera
     }
diff --git a/camera/camera-camera2-pipe-testing/OWNERS b/camera/camera-camera2-pipe-testing/OWNERS
index c445688..9a1b187 100644
--- a/camera/camera-camera2-pipe-testing/OWNERS
+++ b/camera/camera-camera2-pipe-testing/OWNERS
@@ -2,3 +2,5 @@
 [email protected]
 [email protected]
 [email protected]
[email protected]
[email protected]
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/OWNERS b/camera/camera-camera2-pipe/OWNERS
index c445688..9a1b187 100644
--- a/camera/camera-camera2-pipe/OWNERS
+++ b/camera/camera-camera2-pipe/OWNERS
@@ -2,3 +2,5 @@
 [email protected]
 [email protected]
 [email protected]
[email protected]
[email protected]
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 4f42a45..d555c11 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -35,6 +35,7 @@
 import androidx.camera.camera2.pipe.GraphState.GraphStateStarting
 import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
 import androidx.camera.camera2.pipe.GraphState.GraphStateStopping
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode
 import androidx.camera.camera2.pipe.core.Log
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
@@ -154,6 +155,21 @@
     fun setSurface(stream: StreamId, surface: Surface?)
 
     /**
+     * CameraPipe allows setting the global audio restriction through [CameraPipe] and audio
+     * restrictions on individual [CameraGraph]s. When multiple settings are present, the highest
+     * level of audio restriction across global and individual [CameraGraph]s is used as the
+     * device's audio restriction
+     *
+     * Returns the mode of audio restriction associated with the [CameraGraph].
+     */
+    fun getAudioRestriction(): AudioRestrictionMode?
+
+    /**
+     * Sets the audio restriction of CameraGraph.
+     */
+    fun setAudioRestriction(mode: AudioRestrictionMode)
+
+    /**
      * This defines the configuration, flags, and pre-defined structure of a [CameraGraph] instance.
      * Note that for parameters, null is considered a valid value, and unset keys are ignored.
      *
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AudioRestrictionController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AudioRestrictionController.kt
new file mode 100644
index 0000000..481baa9
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AudioRestrictionController.kt
@@ -0,0 +1,109 @@
+/*
+* Copyright 2024 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package androidx.camera.camera2.pipe.compat
+
+import android.hardware.camera2.CameraDevice
+import androidx.annotation.GuardedBy
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_NONE
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_VIBRATION
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_VIBRATION_SOUND
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Class that keeps the global audio restriction mode and audio restriction mode on each
+ * CameraGraph, and computes the final audio restriction mode based on the settings.
+ */
+@Singleton
+@RequiresApi(30)
+class AudioRestrictionController @Inject constructor() {
+    private val lock = Any()
+    var globalAudioRestrictionMode: AudioRestrictionMode = AUDIO_RESTRICTION_NONE
+        get() = synchronized(lock) { field }
+        set(value: AudioRestrictionMode) {
+            synchronized(lock) {
+                field = value
+                updateListenersMode()
+            }
+        }
+
+    private val audioRestrictionModeMap: MutableMap<CameraGraph, AudioRestrictionMode> =
+        mutableMapOf()
+    private val activeListeners: MutableSet<Listener> = mutableSetOf()
+
+    fun getCameraGraphAudioRestriction(cameraGraph: CameraGraph): AudioRestrictionMode {
+        return audioRestrictionModeMap.getOrDefault(cameraGraph, AUDIO_RESTRICTION_NONE)
+    }
+
+    fun setCameraGraphAudioRestriction(cameraGraph: CameraGraph, mode: AudioRestrictionMode) {
+        synchronized(lock) {
+            audioRestrictionModeMap[cameraGraph] = mode
+            updateListenersMode()
+        }
+    }
+
+    fun removeCameraGraph(cameraGraph: CameraGraph) {
+        synchronized(lock) {
+            audioRestrictionModeMap.remove(cameraGraph)
+            updateListenersMode()
+        }
+    }
+
+    @GuardedBy("lock")
+    private fun computeAudioRestrictionMode(): AudioRestrictionMode {
+        if (audioRestrictionModeMap.containsValue(AUDIO_RESTRICTION_VIBRATION_SOUND) ||
+            globalAudioRestrictionMode == AUDIO_RESTRICTION_VIBRATION_SOUND
+        ) {
+            return AUDIO_RESTRICTION_VIBRATION_SOUND
+        }
+        if (audioRestrictionModeMap.containsValue(AUDIO_RESTRICTION_VIBRATION) ||
+            globalAudioRestrictionMode == AUDIO_RESTRICTION_VIBRATION
+        ) {
+            return AUDIO_RESTRICTION_VIBRATION
+        }
+        return AUDIO_RESTRICTION_NONE
+    }
+
+    fun addListener(listener: Listener) {
+        synchronized(lock) {
+            activeListeners.add(listener)
+            val mode = computeAudioRestrictionMode()
+            listener.onCameraAudioRestrictionUpdated(mode)
+        }
+    }
+
+    fun removeListener(listener: Listener?) {
+        synchronized(lock) {
+            activeListeners.remove(listener)
+        }
+    }
+
+    @GuardedBy("lock")
+    private fun updateListenersMode() {
+        val mode = computeAudioRestrictionMode()
+        for (listener in activeListeners) {
+            listener.onCameraAudioRestrictionUpdated(mode)
+        }
+    }
+
+    interface Listener {
+        /** @see CameraDevice.getCameraAudioRestriction */
+        fun onCameraAudioRestrictionUpdated(mode: AudioRestrictionMode)
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
index abf3a2f..58d1e70 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
@@ -19,6 +19,7 @@
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCaptureSession
 import android.hardware.camera2.CameraDevice
+import android.os.Build
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraId
@@ -37,6 +38,7 @@
         cameraDevice: CameraDevice? = null,
         closeUnderError: Boolean = false,
         androidCameraState: AndroidCameraState,
+        audioRestriction: AudioRestrictionController?
     )
 }
 
@@ -51,6 +53,7 @@
         cameraDevice: CameraDevice?,
         closeUnderError: Boolean,
         androidCameraState: AndroidCameraState,
+        audioRestriction: AudioRestrictionController?
     ) {
         Log.debug { "Closing $cameraDeviceWrapper and/or $cameraDevice" }
         val unwrappedCameraDevice = cameraDeviceWrapper?.unwrapAs(CameraDevice::class)
@@ -61,14 +64,32 @@
                         "but the accompanied camera device has camera ID ${it.id}"
                 }
             }
-            closeCameraDevice(unwrappedCameraDevice, closeUnderError, androidCameraState)
+            closeCameraDevice(
+                unwrappedCameraDevice,
+                closeUnderError,
+                androidCameraState
+            )
             cameraDeviceWrapper.onDeviceClosed()
+            /**
+             * Only remove the audio restriction when CameraDeviceWrapper is present.
+             * When closeCamera is called without a CameraDeviceWrapper, that means a wrapper
+             * hadn't been created for the opened camera.
+             */
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                audioRestriction?.removeListener(cameraDeviceWrapper)
+            }
 
             // We only need to close the device once (don't want to create another capture session).
             // Return here.
             return
         }
-        cameraDevice?.let { closeCameraDevice(it, closeUnderError, androidCameraState) }
+        cameraDevice?.let {
+            closeCameraDevice(
+                it,
+                closeUnderError,
+                androidCameraState
+            )
+        }
     }
 
     private fun closeCameraDevice(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
index fd97a9b..ab0a826 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
@@ -50,7 +50,7 @@
  * This interface has been modified to correct nullness, adjust exceptions, and to return or produce
  * wrapper interfaces instead of the native Camera2 types.
  */
-internal interface CameraDeviceWrapper : UnsafeWrapper {
+internal interface CameraDeviceWrapper : UnsafeWrapper, AudioRestrictionController.Listener {
     /** @see [CameraDevice.getId] */
     val cameraId: CameraId
 
@@ -110,11 +110,7 @@
 
     /** @see CameraDevice.getCameraAudioRestriction */
     @RequiresApi(Build.VERSION_CODES.R)
-    fun getCameraAudioRestriction(): AudioRestrictionMode
-
-    /** @see CameraDevice.setCameraAudioRestriction */
-    @RequiresApi(Build.VERSION_CODES.R)
-    fun setCameraAudioRestriction(mode: AudioRestrictionMode)
+    fun getCameraAudioRestriction(): AudioRestrictionMode?
 }
 
 internal fun CameraDevice?.closeWithTrace() {
@@ -466,7 +462,7 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.R)
-    override fun setCameraAudioRestriction(mode: AudioRestrictionMode) {
+    override fun onCameraAudioRestrictionUpdated(mode: AudioRestrictionMode) {
         Api30Compat.setCameraAudioRestriction(cameraDevice, mode.value)
     }
 
@@ -647,11 +643,14 @@
     }
 
     @RequiresApi(30)
-    override fun setCameraAudioRestriction(mode: AudioRestrictionMode) {
-        androidCameraDevice.setCameraAudioRestriction(mode)
+    override fun onCameraAudioRestrictionUpdated(mode: AudioRestrictionMode) {
+        androidCameraDevice.onCameraAudioRestrictionUpdated(mode)
     }
 }
 
+/**
+ * @see [CameraDevice.AUDIO_RESTRICTION_NONE] and other constants.
+ */
 @JvmInline
 value class AudioRestrictionMode(val value: Int) {
     companion object {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
index 4a8dfcd..2bd655f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
@@ -173,6 +173,7 @@
         cameraId: CameraId,
         attempts: Int,
         requestTimestamp: TimestampNs,
+        audioRestriction: AudioRestrictionController? = null
     ): OpenCameraResult {
         val metadata = camera2MetadataProvider.getCameraMetadata(cameraId)
         val cameraState =
@@ -186,7 +187,10 @@
                 camera2DeviceCloser,
                 threads,
                 cameraInteropConfig?.cameraDeviceStateCallback,
-                cameraInteropConfig?.cameraSessionStateCallback
+                cameraInteropConfig?.cameraSessionStateCallback,
+                /** interopExtensionSessionStateCallback= */
+                null,
+                audioRestriction
             )
 
         try {
@@ -229,6 +233,7 @@
     private val timeSource: TimeSource,
     private val devicePolicyManager: DevicePolicyManagerWrapper,
     private val cameraInteropConfig: CameraPipe.CameraInteropConfig?,
+    private val audioRestriction: AudioRestrictionController? = null
 ) {
     internal suspend fun openCameraWithRetry(
         cameraId: CameraId,
@@ -245,6 +250,7 @@
                     cameraId,
                     attempts,
                     requestTimestamp,
+                    audioRestriction
                 )
             val elapsed = Timestamps.now(timeSource) - requestTimestamp
             with(result) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
index eb3baea..84c7194 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
@@ -22,6 +22,7 @@
 import android.hardware.camera2.CameraCaptureSession.StateCallback
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CameraExtensionSession
+import android.os.Build
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraError
@@ -252,7 +253,8 @@
     private val threads: Threads,
     private val interopDeviceStateCallback: CameraDevice.StateCallback? = null,
     private val interopSessionStateCallback: StateCallback? = null,
-    private val interopExtensionSessionStateCallback: CameraExtensionSession.StateCallback? = null
+    private val interopExtensionSessionStateCallback: CameraExtensionSession.StateCallback? = null,
+    private val audioRestriction: AudioRestrictionController? = null
 ) : CameraDevice.StateCallback() {
     private val debugId = androidCameraDebugIds.incrementAndGet()
     private val lock = Any()
@@ -334,25 +336,28 @@
             camera2DeviceCloser.closeCamera(
                 cameraDevice = cameraDevice,
                 closeUnderError = currentCloseInfo.errorCode != null,
-                androidCameraState = this
+                androidCameraState = this,
+                audioRestriction = audioRestriction
             )
             return
         }
 
         // Update _state.value _without_ holding the lock. This may block the calling thread for a
         // while if it synchronously calls createCaptureSession.
+        val androidCameraDevice = AndroidCameraDevice(
+            metadata,
+            cameraDevice,
+            cameraId,
+            cameraErrorListener,
+            interopSessionStateCallback,
+            interopExtensionSessionStateCallback,
+            threads
+        )
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            audioRestriction?.addListener(androidCameraDevice)
+        }
         _state.value =
-            CameraStateOpen(
-                AndroidCameraDevice(
-                    metadata,
-                    cameraDevice,
-                    cameraId,
-                    cameraErrorListener,
-                    interopSessionStateCallback,
-                    interopExtensionSessionStateCallback,
-                    threads
-                )
-            )
+            CameraStateOpen(androidCameraDevice)
 
         // Check to see if we received close() or other events in the meantime.
         val closeInfo =
@@ -365,7 +370,8 @@
             camera2DeviceCloser.closeCamera(
                 cameraDevice = cameraDevice,
                 closeUnderError = closeInfo.errorCode != null,
-                androidCameraState = this
+                androidCameraState = this,
+                audioRestriction = audioRestriction
             )
             _state.value = computeClosedState(closeInfo)
         }
@@ -471,6 +477,7 @@
                 cameraDevice,
                 closeUnderError = closeInfo.errorCode != null,
                 androidCameraState = this,
+                audioRestriction = audioRestriction
             )
             _state.value = computeClosedState(closeInfo)
         }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
index b9f4539..903b122 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
@@ -26,6 +26,8 @@
 import androidx.camera.camera2.pipe.GraphState
 import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode
 import androidx.camera.camera2.pipe.config.CameraGraphScope
 import androidx.camera.camera2.pipe.core.Debug
 import androidx.camera.camera2.pipe.core.Log
@@ -67,6 +69,7 @@
     private val listener3A: Listener3A,
     private val frameDistributor: FrameDistributor,
     private val frameCaptureQueue: FrameCaptureQueue,
+    private val audioRestriction: AudioRestrictionController? = null
 ) : CameraGraph {
     private val debugId = cameraGraphIds.incrementAndGet()
     private val sessionMutex = Mutex()
@@ -206,6 +209,19 @@
         Debug.traceStop()
     }
 
+    override fun getAudioRestriction(): AudioRestrictionMode? {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            return audioRestriction?.getCameraGraphAudioRestriction(this)
+        }
+        return null
+    }
+
+    override fun setAudioRestriction(mode: AudioRestrictionMode) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            audioRestriction?.setCameraGraphAudioRestriction(this, mode)
+        }
+    }
+
     override fun close() {
         if (closed.compareAndSet(expect = false, update = true)) {
             Debug.traceStart { "$this#close" }
@@ -215,6 +231,9 @@
             frameDistributor.close()
             frameCaptureQueue.close()
             surfaceGraph.close()
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                audioRestriction?.removeCameraGraph(this)
+            }
             Debug.traceStop()
         }
     }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AudioRestrictionControllerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AudioRestrictionControllerTest.kt
new file mode 100644
index 0000000..5421440
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AudioRestrictionControllerTest.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.compat
+
+import android.os.Build
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_VIBRATION
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_VIBRATION_SOUND
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.R)
+class AudioRestrictionControllerTest {
+    private val cameraGraph1: CameraGraph = mock()
+    private val cameraGraph2: CameraGraph = mock()
+    private val listener1: AudioRestrictionController.Listener = mock()
+    private val listener2: AudioRestrictionController.Listener = mock()
+
+    @Test
+    fun setAudioRestrictionMode_ListenerUpdatedToHighestMode() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+        audioRestriction.addListener(listener2)
+
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph1, AUDIO_RESTRICTION_VIBRATION)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(AUDIO_RESTRICTION_VIBRATION)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(AUDIO_RESTRICTION_VIBRATION)
+
+        audioRestriction.setCameraGraphAudioRestriction(
+            cameraGraph2,
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+    }
+
+    @Test
+    fun setGlobalAudioRestrictionMode_ListenerUpdatedToHighestMode() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+        audioRestriction.addListener(listener2)
+
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph1, AUDIO_RESTRICTION_VIBRATION)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(AUDIO_RESTRICTION_VIBRATION)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(AUDIO_RESTRICTION_VIBRATION)
+
+        audioRestriction.globalAudioRestrictionMode = AUDIO_RESTRICTION_VIBRATION_SOUND
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+    }
+
+    @Test
+    fun setAudioRestrictionMode_lowerModeNotOverrideHigherMode() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(
+            cameraGraph1,
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph2, AUDIO_RESTRICTION_VIBRATION)
+
+        // Whenever a setter method is called, an update should be called on the listener
+        verify(listener1, times(2)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        verify(listener1, never()).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION
+        )
+    }
+
+    @Test
+    fun setGlobalAudioRestrictionMode_lowerModeNotOverrideHigherMode() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(
+            cameraGraph1,
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        audioRestriction.globalAudioRestrictionMode = AUDIO_RESTRICTION_VIBRATION
+
+        // Whenever a setter method is called, an update should be called on the listener
+        verify(listener1, times(2)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        verify(listener1, never()).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION
+        )
+    }
+
+    @Test
+    fun removeCameraGraphAudioRestriction_associatedModeUpdated() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(
+            cameraGraph1,
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph2, AUDIO_RESTRICTION_VIBRATION)
+
+        verify(listener1, times(2)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+
+        audioRestriction.removeCameraGraph(cameraGraph1)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION
+        )
+    }
+
+    @Test
+    fun addListenerAfterUpdateMode_newListenerUpdated() {
+        val mode = AUDIO_RESTRICTION_VIBRATION
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph1, mode)
+        audioRestriction.addListener(listener2)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(mode)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(mode)
+    }
+
+    @Test
+    fun setRestrictionBeforeAddingListener_listenerSetToUpdatedMode() {
+        val mode = AUDIO_RESTRICTION_VIBRATION
+        val audioRestriction = AudioRestrictionController()
+
+        audioRestriction.globalAudioRestrictionMode = mode
+        audioRestriction.addListener(listener1)
+        audioRestriction.addListener(listener2)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(mode)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(mode)
+    }
+
+    @Test
+    fun removedListener_noLongerUpdated() {
+        val mode = AUDIO_RESTRICTION_VIBRATION
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+        audioRestriction.addListener(listener2)
+        audioRestriction.removeListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph1, mode)
+
+        verify(listener1, times(0)).onCameraAudioRestrictionUpdated(mode)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(mode)
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
index 24c59ce..44ee601 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
@@ -29,6 +29,8 @@
 import androidx.camera.camera2.pipe.CameraSurfaceManager
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode
 import androidx.camera.camera2.pipe.internal.CameraBackendsImpl
 import androidx.camera.camera2.pipe.internal.FrameCaptureQueue
 import androidx.camera.camera2.pipe.internal.FrameDistributor
@@ -42,6 +44,7 @@
 import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
@@ -113,6 +116,13 @@
             cameraSurfaceManager,
             emptyMap()
         )
+        val audioRestriction: AudioRestrictionController? =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                AudioRestrictionController()
+            } else {
+                null
+            }
+
         val graph =
             CameraGraphImpl(
                 graphConfig,
@@ -127,7 +137,8 @@
                 GraphState3A(),
                 Listener3A(),
                 frameDistributor,
-                frameCaptureQueue
+                frameCaptureQueue,
+                audioRestriction
             )
         stream1 =
             checkNotNull(graph.streams[stream1Config]) {
@@ -259,4 +270,13 @@
         verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(imageReader1.surface))
         verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(imageReader1.surface))
     }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.R)
+    fun setAudioRestriction_setValueSuccessfully() = runTest {
+        val mode = AudioRestrictionMode(0)
+        val cameraGraph = initializeCameraGraphImpl(this)
+        cameraGraph.setAudioRestriction(mode)
+        assertEquals(mode, cameraGraph.getAudioRestriction())
+    }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCamera2DeviceCloser.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCamera2DeviceCloser.kt
index 7b6cc46..6236a42 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCamera2DeviceCloser.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCamera2DeviceCloser.kt
@@ -17,16 +17,21 @@
 package androidx.camera.camera2.pipe.testing
 
 import android.hardware.camera2.CameraDevice
+import android.os.Build
+import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.compat.AndroidCameraState
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
 import androidx.camera.camera2.pipe.compat.Camera2DeviceCloser
 import androidx.camera.camera2.pipe.compat.CameraDeviceWrapper
 
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
 internal class FakeCamera2DeviceCloser : Camera2DeviceCloser {
     override fun closeCamera(
         cameraDeviceWrapper: CameraDeviceWrapper?,
         cameraDevice: CameraDevice?,
         closeUnderError: Boolean,
         androidCameraState: AndroidCameraState,
+        audioRestriction: AudioRestrictionController?
     ) {
         cameraDeviceWrapper?.onDeviceClosed()
     }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
index ff8f9d0..38d1748 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
@@ -120,7 +120,7 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.R)
-    override fun setCameraAudioRestriction(mode: AudioRestrictionMode) {
+    override fun onCameraAudioRestrictionUpdated(mode: AudioRestrictionMode) {
         fakeCamera.cameraDevice.cameraAudioRestriction = mode.value
     }
 
diff --git a/camera/camera-camera2/api/current.txt b/camera/camera-camera2/api/current.txt
index 87c79d0..1f2bf8d 100644
--- a/camera/camera-camera2/api/current.txt
+++ b/camera/camera-camera2/api/current.txt
@@ -40,7 +40,7 @@
     method public <ValueT> ValueT? getCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
   }
 
-  @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.interop.CaptureRequestOptions> {
+  @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.interop.CaptureRequestOptions!> {
     ctor public CaptureRequestOptions.Builder();
     method public androidx.camera.camera2.interop.CaptureRequestOptions build();
     method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder clearCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
diff --git a/camera/camera-camera2/api/restricted_current.txt b/camera/camera-camera2/api/restricted_current.txt
index 87c79d0..1f2bf8d 100644
--- a/camera/camera-camera2/api/restricted_current.txt
+++ b/camera/camera-camera2/api/restricted_current.txt
@@ -40,7 +40,7 @@
     method public <ValueT> ValueT? getCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
   }
 
-  @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.interop.CaptureRequestOptions> {
+  @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.interop.CaptureRequestOptions!> {
     ctor public CaptureRequestOptions.Builder();
     method public androidx.camera.camera2.interop.CaptureRequestOptions build();
     method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder clearCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 5bc7186..e6361d0 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -1067,7 +1067,7 @@
                     removeMeteringRepeating();
                 } else {
                     // Other normal cases, do nothing.
-                    Logger.d(TAG, "mMeteringRepeating is ATTACHED, "
+                    Logger.d(TAG, "No need to remove a previous mMeteringRepeating, "
                             + "SessionConfig Surfaces: " + sizeSessionSurfaces + ", "
                             + "CaptureConfig Surfaces: " + sizeRepeatingSurfaces);
                 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index 1e8e2fc..ee7006c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -212,10 +212,12 @@
 
                             mProcessorState = ProcessorState.SESSION_INITIALIZED;
                             try {
-                                DeferrableSurfaces.incrementAll(mOutputSurfaces);
+                                List<DeferrableSurface> surfacesToIncrement =
+                                        new ArrayList<>(mOutputSurfaces);
                                 if (postviewDeferrableSurface != null) {
-                                    postviewDeferrableSurface.incrementUseCount();
+                                    surfacesToIncrement.add(postviewDeferrableSurface);
                                 }
+                                DeferrableSurfaces.incrementAll(surfacesToIncrement);
                             } catch (DeferrableSurface.SurfaceClosedException e) {
                                 return Futures.immediateFailedFuture(e);
                             }
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 2a567c8..abd42142 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -277,7 +277,7 @@
     method public default void updateTransform(android.graphics.Matrix?);
   }
 
-  public static final class ImageAnalysis.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageAnalysis> {
+  public static final class ImageAnalysis.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageAnalysis!> {
     ctor public ImageAnalysis.Builder();
     method public androidx.camera.core.ImageAnalysis build();
     method public androidx.camera.core.ImageAnalysis.Builder setBackgroundExecutor(java.util.concurrent.Executor);
@@ -324,7 +324,7 @@
     field public static final int FLASH_MODE_SCREEN = 3; // 0x3
   }
 
-  public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture> {
+  public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
     ctor public ImageCapture.Builder();
     method public androidx.camera.core.ImageCapture build();
     method public androidx.camera.core.ImageCapture.Builder setCaptureMode(int);
@@ -485,7 +485,7 @@
     method public void setTargetRotation(int);
   }
 
-  public static final class Preview.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.Preview> {
+  public static final class Preview.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.Preview!> {
     ctor public Preview.Builder();
     method public androidx.camera.core.Preview build();
     method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 2a567c8..abd42142 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -277,7 +277,7 @@
     method public default void updateTransform(android.graphics.Matrix?);
   }
 
-  public static final class ImageAnalysis.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageAnalysis> {
+  public static final class ImageAnalysis.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageAnalysis!> {
     ctor public ImageAnalysis.Builder();
     method public androidx.camera.core.ImageAnalysis build();
     method public androidx.camera.core.ImageAnalysis.Builder setBackgroundExecutor(java.util.concurrent.Executor);
@@ -324,7 +324,7 @@
     field public static final int FLASH_MODE_SCREEN = 3; // 0x3
   }
 
-  public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture> {
+  public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
     ctor public ImageCapture.Builder();
     method public androidx.camera.core.ImageCapture build();
     method public androidx.camera.core.ImageCapture.Builder setCaptureMode(int);
@@ -485,7 +485,7 @@
     method public void setTargetRotation(int);
   }
 
-  public static final class Preview.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.Preview> {
+  public static final class Preview.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.Preview!> {
     ctor public Preview.Builder();
     method public androidx.camera.core.Preview build();
     method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
index 1046b45..6c3cf5d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
@@ -323,10 +323,16 @@
         // prematurely before it can be used by camera2.
         inputEdge.getSurface().getTerminationFuture().addListener(() -> {
             imageReader.safeClose();
-            if (imageReaderForPostview != null) {
-                imageReaderForPostview.safeClose();
-            }
         }, mainThreadExecutor());
+
+        if (inputEdge.getPostviewSurface() != null) {
+            inputEdge.getPostviewSurface().close();
+            inputEdge.getPostviewSurface().getTerminationFuture().addListener(() -> {
+                if (imageReaderForPostview != null) {
+                    imageReaderForPostview.safeClose();
+                }
+            }, mainThreadExecutor());
+        }
     }
 
     @VisibleForTesting
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
index 24baeae..f9f6649 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
@@ -247,7 +247,14 @@
                 || Build.MODEL.equalsIgnoreCase("G8342")))
                 || Build.MODEL.contains("Cuttlefish")
                 || Build.MODEL.equalsIgnoreCase("Pixel XL")
-                || Build.MODEL.equalsIgnoreCase("Pixel");
+                || Build.MODEL.equalsIgnoreCase("Pixel")
+                // Skip all devices that have ExtraCropping Quirk
+                || Build.MODEL.equalsIgnoreCase("SM-T580")
+                || Build.MODEL.equalsIgnoreCase("SM-J710MN")
+                || Build.MODEL.equalsIgnoreCase("SM-A320FL")
+                || Build.MODEL.equalsIgnoreCase("SM-G570M")
+                || Build.MODEL.equalsIgnoreCase("SM-G610F")
+                || Build.MODEL.equalsIgnoreCase("SM-G610M");
     }
 
     /**
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
index bde753e..801976c 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
@@ -54,6 +54,7 @@
     private static final int DEFAULT_STAGE_ID = 0;
     private static final int SESSION_STAGE_ID = 101;
     private static final int EFFECT = CaptureRequest.CONTROL_EFFECT_MODE_SOLARIZE;
+    private AutoImageCaptureExtenderCaptureProcessorImpl mCaptureProcessor = null;
 
     public AutoImageCaptureExtenderImpl() {
     }
@@ -93,7 +94,8 @@
     @Override
     public CaptureProcessorImpl getCaptureProcessor() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return new AutoImageCaptureExtenderCaptureProcessorImpl();
+            mCaptureProcessor = new AutoImageCaptureExtenderCaptureProcessorImpl();
+            return mCaptureProcessor;
         } else {
             return new NoOpCaptureProcessorImpl();
         }
@@ -108,7 +110,9 @@
 
     @Override
     public void onDeInit() {
-
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mCaptureProcessor != null) {
+            mCaptureProcessor.release();
+        }
     }
 
     @Nullable
@@ -256,6 +260,7 @@
                         }
                     }
                 }
+                outputImage.setTimestamp(image.getTimestamp());
                 mImageWriter.queueInputImage(outputImage);
             }
 
@@ -294,6 +299,12 @@
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
             throw new UnsupportedOperationException("Postview is not supported");
         }
+
+        public void release() {
+            if (mImageWriter != null) {
+                mImageWriter.close();
+            }
+        }
     }
 
     @NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
index 8e391ea..4647a5b 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
@@ -60,6 +60,7 @@
     private static final int EFFECT = CaptureRequest.CONTROL_EFFECT_MODE_NEGATIVE;
 
     private CameraCharacteristics mCameraCharacteristics;
+    private BeautyImageCaptureExtenderCaptureProcessorImpl mCaptureProcessor;
 
     public BeautyImageCaptureExtenderImpl() {
     }
@@ -100,7 +101,8 @@
     @Override
     public CaptureProcessorImpl getCaptureProcessor() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return new BeautyImageCaptureExtenderCaptureProcessorImpl();
+            mCaptureProcessor = new BeautyImageCaptureExtenderCaptureProcessorImpl();
+            return mCaptureProcessor;
         } else {
             return new NoOpCaptureProcessorImpl();
         }
@@ -115,7 +117,9 @@
 
     @Override
     public void onDeInit() {
-
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mCaptureProcessor != null) {
+            mCaptureProcessor.release();
+        }
     }
 
     @Nullable
@@ -277,6 +281,7 @@
                         }
                     }
                 }
+                outputImage.setTimestamp(image.getTimestamp());
                 mImageWriter.queueInputImage(outputImage);
             }
 
@@ -315,6 +320,12 @@
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
             throw new UnsupportedOperationException("Postview is not supported");
         }
+
+        public void release() {
+            if (mImageWriter != null) {
+                mImageWriter.close();
+            }
+        }
     }
 
     @NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index a2f565c..9afae48 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -56,7 +56,7 @@
     private static final int DEFAULT_STAGE_ID = 0;
     private static final int SESSION_STAGE_ID = 101;
     private static final int EFFECT = CaptureRequest.CONTROL_EFFECT_MODE_SEPIA;
-
+    private BokehImageCaptureExtenderCaptureProcessorImpl mCaptureProcessor;
     public BokehImageCaptureExtenderImpl() {
     }
 
@@ -95,7 +95,8 @@
     @Override
     public CaptureProcessorImpl getCaptureProcessor() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return new BokehImageCaptureExtenderCaptureProcessorImpl();
+            mCaptureProcessor = new BokehImageCaptureExtenderCaptureProcessorImpl();
+            return mCaptureProcessor;
         } else {
             return new NoOpCaptureProcessorImpl();
         }
@@ -110,7 +111,9 @@
 
     @Override
     public void onDeInit() {
-
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mCaptureProcessor != null) {
+            mCaptureProcessor.release();
+        }
     }
 
     @Nullable
@@ -257,6 +260,7 @@
                         }
                     }
                 }
+                outputImage.setTimestamp(image.getTimestamp());
                 mImageWriter.queueInputImage(outputImage);
             }
 
@@ -295,6 +299,12 @@
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
             throw new UnsupportedOperationException("Postview is not supported");
         }
+
+        public void release() {
+            if (mImageWriter != null) {
+                mImageWriter.close();
+            }
+        }
     }
 
     @NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index 677f4de..20f9124 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -257,6 +257,7 @@
             // Do processing here
             // The sample here simply returns the normal image result
             Image normalImage = imageDataPairs.get(NORMAL_STAGE_ID).first;
+            outputImage.setTimestamp(imageDataPairs.get(UNDER_STAGE_ID).first.getTimestamp());
             if (outputImage.getWidth() != normalImage.getWidth()
                     || outputImage.getHeight() != normalImage.getHeight()) {
                 throw new IllegalStateException(String.format("input image "
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
index 561e6c5..8d9940a 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
@@ -271,7 +271,7 @@
             List<Pair<Image, TotalCaptureResult>> imageDataPairs = new ArrayList<>(
                     results.values());
             Image outputImage = mImageWriter.dequeueInputImage();
-
+            outputImage.setTimestamp(imageDataPairs.get(0).first.getTimestamp());
             // Do processing here
             // The sample here simply returns the normal image result
             int stageId = DEFAULT_STAGE_ID;
@@ -300,7 +300,6 @@
                         outYBuffer.put(outIndex, inYBuffer.get(inIndex));
                     }
                 }
-
                 if (resultCallback != null) {
                     executorForCallback.execute(
                             () -> resultCallback.onCaptureProcessProgressed(50));
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index df297e5..405c299 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -167,7 +167,7 @@
     method public static <T extends androidx.camera.video.VideoOutput> androidx.camera.video.VideoCapture<T!> withOutput(T);
   }
 
-  @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture> {
+  @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture!> {
     ctor public VideoCapture.Builder(T);
     method public androidx.camera.video.VideoCapture<T!> build();
     method public androidx.camera.video.VideoCapture.Builder<T!> setDynamicRange(androidx.camera.core.DynamicRange);
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index df297e5..405c299 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -167,7 +167,7 @@
     method public static <T extends androidx.camera.video.VideoOutput> androidx.camera.video.VideoCapture<T!> withOutput(T);
   }
 
-  @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture> {
+  @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture!> {
     ctor public VideoCapture.Builder(T);
     method public androidx.camera.video.VideoCapture<T!> build();
     method public androidx.camera.video.VideoCapture.Builder<T!> setDynamicRange(androidx.camera.core.DynamicRange);
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
index eabc522b..cdc6214 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
@@ -37,11 +37,11 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
 
 @Composable
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
index 1f54e41..f6ed832 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
@@ -55,10 +55,10 @@
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.Observer
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 private const val TAG = "ImageCaptureScreen"
 
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
index 71f10a5..4bf2a6e 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
@@ -47,10 +47,10 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.Observer
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 private const val TAG = "VideoCaptureScreen"
 
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/viewfinder/ViewfinderScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/viewfinder/ViewfinderScreen.kt
index 0c95af8..73e13bb 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/viewfinder/ViewfinderScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/viewfinder/ViewfinderScreen.kt
@@ -41,9 +41,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 private const val TAG = "ViewfinderScreen"
 
diff --git a/car/app/app/src/main/java/androidx/car/app/CarContext.java b/car/app/app/src/main/java/androidx/car/app/CarContext.java
index 492fb78..3a52126 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarContext.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarContext.java
@@ -25,6 +25,7 @@
 import static java.util.Objects.requireNonNull;
 
 import android.app.Activity;
+import android.app.ActivityOptions;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -32,15 +33,20 @@
 import android.content.res.Configuration;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.util.Log;
+import android.view.Display;
 
 import androidx.activity.OnBackPressedCallback;
 import androidx.activity.OnBackPressedDispatcher;
+import androidx.annotation.DoNotInline;
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.StringDef;
 import androidx.car.app.annotations.ExperimentalCarApi;
@@ -617,7 +623,11 @@
                 new Intent(REQUEST_PERMISSIONS_ACTION).setComponent(appActivityComponent)
                         .putExtras(extras)
                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        startActivity(intent);
+        Bundle activityOptionsBundle = null;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            activityOptionsBundle = Api26Impl.makeBasicActivityOptionsBundle();
+        }
+        startActivity(intent, activityOptionsBundle);
     }
 
     @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
@@ -756,4 +766,14 @@
 
         lifecycle.addObserver(observer);
     }
+
+    @RequiresApi(api = VERSION_CODES.O)
+    private static class Api26Impl {
+
+        @DoNotInline
+        static Bundle makeBasicActivityOptionsBundle() {
+            return ActivityOptions.makeBasic()
+                    .setLaunchDisplayId(Display.DEFAULT_DISPLAY).toBundle();
+        }
+    }
 }
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt
index 9c394df..61aef58 100644
--- a/collection/collection/api/current.txt
+++ b/collection/collection/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.collection {
 
-  public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+  public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K!,V!> implements java.util.Map<K!,V!> {
     ctor public ArrayMap();
     ctor public ArrayMap(androidx.collection.SimpleArrayMap?);
     ctor public ArrayMap(int);
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt
index 042ced8..0c8d12c 100644
--- a/collection/collection/api/restricted_current.txt
+++ b/collection/collection/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.collection {
 
-  public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+  public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K!,V!> implements java.util.Map<K!,V!> {
     ctor public ArrayMap();
     ctor public ArrayMap(androidx.collection.SimpleArrayMap?);
     ctor public ArrayMap(int);
diff --git a/collection/integration-tests/testapp/src/main/kotlin/androidx/collection/integration/ArraySetKotlin.kt b/collection/integration-tests/testapp/src/main/kotlin/androidx/collection/integration/ArraySetKotlin.kt
index e43a927..d498a60 100644
--- a/collection/integration-tests/testapp/src/main/kotlin/androidx/collection/integration/ArraySetKotlin.kt
+++ b/collection/integration-tests/testapp/src/main/kotlin/androidx/collection/integration/ArraySetKotlin.kt
@@ -18,7 +18,6 @@
 
 import androidx.collection.ArraySet
 import androidx.collection.arraySetOf
-import java.util.function.IntFunction
 
 /**
  * Integration (actually build) test for source compatibility for usages of ArraySet.
@@ -44,13 +43,10 @@
     val array = Array(arraySet.size) { 0 }
     arraySet.forEachIndexed(array::set) // Copy into an existing array
 
-    @Suppress("RedundantSamConstructor", "DEPRECATION")
     return arraySet.isEmpty() && arraySet.remove(0) &&
         arraySet.removeAll(arraySetOf(1, 2)) && arraySet.removeAll(listOf(1, 2)) &&
         arraySet.removeAt(0) == 0 && arraySet.contains(0) && arraySet.size == 0 &&
         arraySet.isEmpty() && arraySet.toArray() === arraySet.toArray(arrayOf<Number>()) &&
         arraySet + arrayOf(1) == arraySet - arrayOf(1) && arraySet == arrayOf(0) &&
-        arraySet.toArray { value -> arrayOf(value) }.equals(
-            arraySet.toArray(IntFunction { value -> arrayOf(value) })
-        ) && arraySet.containsAll(listOf(1, 2))
+        arraySet.containsAll(listOf(1, 2))
 }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index c5c7fac..08676bb 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -287,7 +287,7 @@
     companion object {
         fun checkCompilerVersion(configuration: CompilerConfiguration): Boolean {
             try {
-                val KOTLIN_VERSION_EXPECTATION = "1.9.22"
+                val KOTLIN_VERSION_EXPECTATION = "1.9.23"
                 KotlinCompilerVersion.getVersion()?.let { version ->
                     val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
                     val suppressKotlinVersionCheck = configuration.get(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index a0cf17b..cdcea5f 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -146,7 +146,7 @@
          * The maven version string of this compiler. This string should be updated before/after every
          * release.
          */
-        const val compilerVersion: String = "1.5.10"
+        const val compilerVersion: String = "1.5.11"
         private val minimumRuntimeVersion: String
             get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
     }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 51fb33d..82dc8f6 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -63,6 +63,7 @@
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.ir.declarations.IrPackageFragment
 import org.jetbrains.kotlin.ir.declarations.IrProperty
+import org.jetbrains.kotlin.ir.declarations.IrScript
 import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
 import org.jetbrains.kotlin.ir.declarations.IrTypeAlias
 import org.jetbrains.kotlin.ir.declarations.IrTypeParameter
@@ -1947,7 +1948,8 @@
             is IrAnonymousInitializer,
             is IrTypeParameter,
             is IrLocalDelegatedProperty,
-            is IrValueDeclaration -> {
+            is IrValueDeclaration,
+            is IrScript -> {
                 // these declarations do not create new "scopes", so we do nothing
                 return super.visitDeclaration(declaration)
             }
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 9d277bb..eb68fb3 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -320,11 +320,10 @@
   }
 
   public final class ReceiveContentKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier receiveContent(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, androidx.compose.foundation.content.ReceiveContentListener receiveContentListener);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier receiveContent(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.content.TransferableContent,androidx.compose.foundation.content.TransferableContent?> onReceive);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier contentReceiver(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, androidx.compose.foundation.content.ReceiveContentListener receiveContentListener);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface ReceiveContentListener {
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public fun interface ReceiveContentListener {
     method public default void onDragEnd();
     method public default void onDragEnter();
     method public default void onDragExit();
@@ -357,7 +356,7 @@
   }
 
   public final class TransferableContent_androidKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.content.TransferableContent? consumeEach(androidx.compose.foundation.content.TransferableContent, kotlin.jvm.functions.Function1<? super android.content.ClipData.Item,java.lang.Boolean> predicate);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.content.TransferableContent? consume(androidx.compose.foundation.content.TransferableContent, kotlin.jvm.functions.Function1<? super android.content.ClipData.Item,java.lang.Boolean> predicate);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static boolean hasMediaType(androidx.compose.foundation.content.TransferableContent, androidx.compose.foundation.content.MediaType mediaType);
   }
 
@@ -1763,7 +1762,7 @@
   public final class TextFieldStateKt {
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void clearText(androidx.compose.foundation.text.input.TextFieldState);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend Object? forEachTextValue(androidx.compose.foundation.text.input.TextFieldState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.text.input.TextFieldCharSequence,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<?>);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.text.input.TextFieldState rememberTextFieldState(optional String initialText, optional long initialSelection);
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.text.input.TextFieldState rememberTextFieldState(optional String initialText, optional long initialSelection);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndPlaceCursorAtEnd(androidx.compose.foundation.text.input.TextFieldState, String text);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndSelectAll(androidx.compose.foundation.text.input.TextFieldState, String text);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> textAsFlow(androidx.compose.foundation.text.input.TextFieldState);
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index f58c441..7b41db0 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -322,11 +322,10 @@
   }
 
   public final class ReceiveContentKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier receiveContent(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, androidx.compose.foundation.content.ReceiveContentListener receiveContentListener);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier receiveContent(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.content.TransferableContent,androidx.compose.foundation.content.TransferableContent?> onReceive);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier contentReceiver(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, androidx.compose.foundation.content.ReceiveContentListener receiveContentListener);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface ReceiveContentListener {
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public fun interface ReceiveContentListener {
     method public default void onDragEnd();
     method public default void onDragEnter();
     method public default void onDragExit();
@@ -359,7 +358,7 @@
   }
 
   public final class TransferableContent_androidKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.content.TransferableContent? consumeEach(androidx.compose.foundation.content.TransferableContent, kotlin.jvm.functions.Function1<? super android.content.ClipData.Item,java.lang.Boolean> predicate);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.content.TransferableContent? consume(androidx.compose.foundation.content.TransferableContent, kotlin.jvm.functions.Function1<? super android.content.ClipData.Item,java.lang.Boolean> predicate);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static boolean hasMediaType(androidx.compose.foundation.content.TransferableContent, androidx.compose.foundation.content.MediaType mediaType);
   }
 
@@ -1751,6 +1750,7 @@
     ctor public TextFieldState(optional String initialText, optional long initialSelection);
     method @kotlin.PublishedApi internal void commitEdit(androidx.compose.foundation.text.input.TextFieldBuffer newValue);
     method public inline void edit(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.text.input.TextFieldBuffer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal void finishEditing();
     method public androidx.compose.foundation.text.input.TextFieldCharSequence getText();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.text.input.UndoState getUndoState();
     method @kotlin.PublishedApi internal androidx.compose.foundation.text.input.TextFieldBuffer startEdit(androidx.compose.foundation.text.input.TextFieldCharSequence value);
@@ -1767,7 +1767,7 @@
   public final class TextFieldStateKt {
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void clearText(androidx.compose.foundation.text.input.TextFieldState);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend Object? forEachTextValue(androidx.compose.foundation.text.input.TextFieldState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.text.input.TextFieldCharSequence,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<?>);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.text.input.TextFieldState rememberTextFieldState(optional String initialText, optional long initialSelection);
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.text.input.TextFieldState rememberTextFieldState(optional String initialText, optional long initialSelection);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndPlaceCursorAtEnd(androidx.compose.foundation.text.input.TextFieldState, String text);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndSelectAll(androidx.compose.foundation.text.input.TextFieldState, String text);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> textAsFlow(androidx.compose.foundation.text.input.TextFieldState);
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerCarrouselDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerCarrouselDemos.kt
index 536b9355..c17b0b2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerCarrouselDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerCarrouselDemos.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.foundation.demos.pager
 
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.Orientation
@@ -46,7 +45,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 
-@OptIn(ExperimentalFoundationApi::class)
 val Carrousel = listOf(
     ComposableDemo("Horizontal") { HorizontalCarrouselDemo() },
     ComposableDemo("Vertical") { VerticalCarrouselDemo() },
@@ -56,7 +54,6 @@
     }
 )
 
-@OptIn(ExperimentalFoundationApi::class)
 val SnapPositionDemos = listOf(
     ComposableDemo("Snap Position - Start") { HorizontalCarrouselDemo(SnapPosition.Start) },
     ComposableDemo("Snap Position - Center") { HorizontalCarrouselDemo(SnapPosition.Center) },
@@ -64,7 +61,6 @@
     ComposableDemo("Snap Position - Custom") { HorizontalCarrouselDemoWithCustomSnapPosition() },
 )
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun HorizontalCarrouselDemoWithCustomSnapPosition() {
     val pagerState = rememberPagerState { PagesCount }
@@ -113,7 +109,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun HorizontalCarrouselDemo(snapPosition: SnapPosition = SnapPosition.Start) {
     val pagerState = rememberPagerState { PagesCount }
@@ -131,7 +126,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun VerticalCarrouselDemo() {
     val pagerState = rememberPagerState { PagesCount }
@@ -148,7 +142,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun HorizontalCustomPageSizeDemo() {
     val pagerState = rememberPagerState { PagesCount }
@@ -166,7 +159,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun HorizontalCustomPageSizeWithCustomMaxScrollDemo() {
     val pagerState = rememberPagerState { PagesCount }
@@ -207,7 +199,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 private val ThreePagesPerViewport = object : PageSize {
     override fun Density.calculateMainAxisPageSize(
         availableSpace: Int,
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerDemos.kt
index f56d4e1..5ba5ced 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerDemos.kt
@@ -32,7 +32,6 @@
 
 package androidx.compose.foundation.demos.pager
 
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Arrangement
@@ -71,7 +70,6 @@
     DemoCategory("Snap Position", SnapPositionDemos),
 )
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun VerticalPagerDemo() {
     val pagerState = rememberPagerState { PagesCount }
@@ -83,7 +81,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal fun HorizontalPagerDemo() {
     val pagerState = rememberPagerState { PagesCount }
@@ -111,7 +108,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal fun PagerControls(modifier: Modifier = Modifier, pagerState: PagerState) {
     val animationScope = rememberCoroutineScope()
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerStateInteractionsDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerStateInteractionsDemos.kt
index 21e7b58..4fe87a5 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerStateInteractionsDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/pager/PagerStateInteractionsDemos.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.foundation.demos.pager
 
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -43,7 +42,6 @@
     }
 )
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun StateDrivenPage() {
     val pagerState = rememberPagerState { PagesCount }
@@ -59,7 +57,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun StateDrivenPageWithMonitor() {
     val pagerState = rememberPagerState { PagesCount }
@@ -76,7 +73,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun StateMonitoringPager() {
     val pagerState = rememberPagerState { PagesCount }
@@ -91,7 +87,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun PageMonitor(modifier: Modifier, pagerState: PagerState) {
     Column(modifier.fillMaxWidth()) {
@@ -104,7 +99,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun StateMonitoringCustomPageSize() {
     val pagerState = rememberPagerState { PagesCount }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 006e233..dc709d1 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -37,6 +37,7 @@
 import androidx.compose.foundation.samples.BasicTextFieldUndoSample
 import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.integration.demos.common.DemoCategory
+import androidx.compose.ui.text.samples.AnnotatedStringFromHtml
 
 val TextDemos = DemoCategory(
     "Text",
@@ -213,5 +214,6 @@
             )
         ),
         ComposableDemo("Text Pointer Icon") { TextPointerIconDemo() },
+        ComposableDemo("Html") { AnnotatedStringFromHtml() }
     )
 )
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
index d83f3ef..e502d6e 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
@@ -29,9 +29,9 @@
 import androidx.compose.foundation.content.MediaType
 import androidx.compose.foundation.content.ReceiveContentListener
 import androidx.compose.foundation.content.TransferableContent
-import androidx.compose.foundation.content.consumeEach
+import androidx.compose.foundation.content.consume
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.hasMediaType
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -111,7 +111,7 @@
             ): TransferableContent? {
                 val newImageUris = mutableListOf<Uri>()
                 return transferableContent
-                    .consumeEach { item ->
+                    .consume { item ->
                         // this happens in the ui thread, try not to load images here.
                         val isImageBitmap = item.uri?.isImageBitmap(context) ?: false
                         if (isImageBitmap) {
@@ -131,7 +131,7 @@
     Column(
         modifier = Modifier
             .fillMaxSize()
-            .receiveContent(
+            .contentReceiver(
                 hintMediaTypes = setOf(MediaType.Image),
                 receiveContentListener = receiveContentListener
             )
@@ -221,7 +221,7 @@
                         transferableContent
                     } else {
                         var uri: Uri? = null
-                        transferableContent.consumeEach { item ->
+                        transferableContent.consume { item ->
                             // only consume this item if we can read
                             if (item.uri != null && uri == null) {
                                 uri = item.uri
@@ -246,7 +246,7 @@
                 ReceiveContentShowcase(
                     "Text Consumer",
                     MediaType.Text, {
-                        it.consumeEach { item ->
+                        it.consume { item ->
                             val text = item.coerceToText(context)
                             // only consume if it has text in it.
                             !text.isNullOrBlank() && item.uri == null
@@ -412,7 +412,7 @@
 fun Modifier.dropReceiveContent(
     state: ReceiveContentState
 ) = composed {
-    receiveContent(state.hintMediaTypes, state.listener)
+    contentReceiver(state.hintMediaTypes, state.listener)
         .background(
             color = if (state.hovering) {
                 MaterialTheme.colors.secondary
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
index b845444..58dadbf 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
@@ -60,7 +60,6 @@
 import kotlin.math.roundToInt
 import kotlinx.coroutines.launch
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun SimpleHorizontalPagerSample() {
@@ -83,7 +82,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun SimpleVerticalPagerSample() {
@@ -106,7 +104,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun PagerWithStateSample() {
@@ -189,7 +186,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun CustomPageSizeSample() {
@@ -227,7 +223,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun ObservingStateChangesInPagerStateSample() {
@@ -260,7 +255,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun AnimateScrollPageSample() {
@@ -299,7 +293,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun ScrollToPageSample() {
@@ -334,7 +327,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun HorizontalPagerWithScrollableContent() {
@@ -403,7 +395,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Sampled
 @Composable
 fun UsingPagerLayoutInfoForSideEffectSample() {
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ReceiveContentSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ReceiveContentSamples.kt
index 8b01a4e..1587db9 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ReceiveContentSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ReceiveContentSamples.kt
@@ -24,9 +24,9 @@
 import androidx.compose.foundation.content.MediaType
 import androidx.compose.foundation.content.ReceiveContentListener
 import androidx.compose.foundation.content.TransferableContent
-import androidx.compose.foundation.content.consumeEach
+import androidx.compose.foundation.content.consume
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.hasMediaType
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.text.BasicTextField
@@ -55,12 +55,12 @@
         }
         BasicTextField(
             state = state,
-            modifier = Modifier.receiveContent(setOf(MediaType.Image)) { transferableContent ->
+            modifier = Modifier.contentReceiver(setOf(MediaType.Image)) { transferableContent ->
                 if (!transferableContent.hasMediaType(MediaType.Image)) {
-                    return@receiveContent transferableContent
+                    return@contentReceiver transferableContent
                 }
                 val newImages = mutableListOf<ImageBitmap>()
-                transferableContent.consumeEach { item ->
+                transferableContent.consume { item ->
                     // only consume this item if we can read an imageBitmap
                     item.readImageBitmap()?.let { newImages += it; true } ?: false
                 }.also {
@@ -95,7 +95,7 @@
                         else -> MaterialTheme.colors.background
                     }
                 )
-                .receiveContent(
+                .contentReceiver(
                     hintMediaTypes = setOf(MediaType.Image),
                     receiveContentListener = object : ReceiveContentListener {
                         override fun onDragStart() {
@@ -123,7 +123,7 @@
                             }
                             val newImages = mutableListOf<ImageBitmap>()
                             return transferableContent
-                                .consumeEach { item ->
+                                .consume { item ->
                                     // only consume this item if we can read an imageBitmap
                                     item
                                         .readImageBitmap()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/ReceiveContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/ReceiveContentTest.kt
index 91d6be5..a0240e1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/ReceiveContentTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/ReceiveContentTest.kt
@@ -62,9 +62,9 @@
         val listenerCalls = mutableListOf<Int>()
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Video)) { listenerCalls += 3; it }
-                .receiveContent(setOf(MediaType.Audio)) { listenerCalls += 2; it }
-                .receiveContent(setOf(MediaType.Text)) { listenerCalls += 1; it }
+                .contentReceiver(setOf(MediaType.Video)) { listenerCalls += 3; it }
+                .contentReceiver(setOf(MediaType.Audio)) { listenerCalls += 2; it }
+                .contentReceiver(setOf(MediaType.Text)) { listenerCalls += 1; it }
                 .then(TestElement {
                     calculatedReceiveContent = it.getReceiveContentConfiguration()
                     calculatedReceiveContent
@@ -90,27 +90,27 @@
         var textReceived: TransferableContent? = null
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Video)) {
+                .contentReceiver(setOf(MediaType.Video)) {
                     videoReceived = it
-                    val t = it.consumeEach {
+                    val t = it.consume {
                         it.uri
                             ?.toString()
                             ?.contains("video") ?: false
                     }
                     t
                 }
-                .receiveContent(setOf(MediaType.Audio)) {
+                .contentReceiver(setOf(MediaType.Audio)) {
                     audioReceived = it
-                    val t = it.consumeEach {
+                    val t = it.consume {
                         it.uri
                             ?.toString()
                             ?.contains("audio") ?: false
                     }
                     t
                 }
-                .receiveContent(setOf(MediaType.Text)) {
+                .contentReceiver(setOf(MediaType.Text)) {
                     textReceived = it
-                    val t = it.consumeEach { it.text != null }
+                    val t = it.consume { it.text != null }
                     t
                 }
                 .then(TestElement {
@@ -171,7 +171,7 @@
                 Box(modifier = Modifier.then(TestElement {
                     calculatedReceiveContent = it.getReceiveContentConfiguration()
                 }))
-                Box(modifier = Modifier.receiveContent(setOf(MediaType.Text)) { it })
+                Box(modifier = Modifier.contentReceiver(setOf(MediaType.Text)) { it })
             }
         }
 
@@ -192,7 +192,7 @@
                     calculatedReceiveContent = it.getReceiveContentConfiguration()
                 })
             ) {
-                Box(modifier = Modifier.receiveContent(setOf(MediaType.Text)) { it })
+                Box(modifier = Modifier.contentReceiver(setOf(MediaType.Text)) { it })
             }
         }
 
@@ -208,7 +208,7 @@
             ReceiveContentListener { null }
         )
         rule.setContent {
-            Box(modifier = Modifier.receiveContent(emptySet()) { it }) {
+            Box(modifier = Modifier.contentReceiver(emptySet()) { it }) {
                 Box(modifier = Modifier.then(TestElement {
                     calculatedReceiveContent = it.getReceiveContentConfiguration()
                 }))
@@ -227,13 +227,13 @@
         var attached by mutableStateOf(true)
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Video)) { it }
+                .contentReceiver(setOf(MediaType.Video)) { it }
                 .then(if (attached) {
-                    Modifier.receiveContent(setOf(MediaType.Audio)) { it }
+                    Modifier.contentReceiver(setOf(MediaType.Audio)) { it }
                 } else {
                     Modifier
                 })
-                .receiveContent(setOf(MediaType.Text)) { it }
+                .contentReceiver(setOf(MediaType.Text)) { it }
                 .then(TestElement {
                     getReceiveContentConfiguration = {
                         it.getReceiveContentConfiguration()
@@ -267,13 +267,13 @@
         var attached by mutableStateOf(false)
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Video)) { it }
+                .contentReceiver(setOf(MediaType.Video)) { it }
                 .then(if (attached) {
-                    Modifier.receiveContent(setOf(MediaType.Audio)) { it }
+                    Modifier.contentReceiver(setOf(MediaType.Audio)) { it }
                 } else {
                     Modifier
                 })
-                .receiveContent(setOf(MediaType.Text)) { it }
+                .contentReceiver(setOf(MediaType.Text)) { it }
                 .then(TestElement {
                     getReceiveContentConfiguration = {
                         it.getReceiveContentConfiguration()
@@ -307,8 +307,8 @@
         var topMediaTypes by mutableStateOf(setOf(MediaType.Video))
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(topMediaTypes) { it }
-                .receiveContent(setOf(MediaType.Text)) { it }
+                .contentReceiver(topMediaTypes) { it }
+                .contentReceiver(setOf(MediaType.Text)) { it }
                 .then(TestElement {
                     getReceiveContentConfiguration = {
                         it.getReceiveContentConfiguration()
@@ -342,8 +342,8 @@
         var currentMediaTypes by mutableStateOf(setOf(MediaType.Video))
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Image)) { it }
-                .receiveContent(currentMediaTypes) { it }
+                .contentReceiver(setOf(MediaType.Image)) { it }
+                .contentReceiver(currentMediaTypes) { it }
                 .then(TestElement {
                     getReceiveContentConfiguration = {
                         it.getReceiveContentConfiguration()
@@ -379,11 +379,11 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(200.dp)
-                .receiveContent(setOf(MediaType.Video)) { it }
+                .contentReceiver(setOf(MediaType.Video)) { it }
                 .size(100.dp)
-                .receiveContent(setOf(MediaType.Audio)) { it }
+                .contentReceiver(setOf(MediaType.Audio)) { it }
                 .size(50.dp)
-                .receiveContent(setOf(MediaType.Text)) { it }
+                .contentReceiver(setOf(MediaType.Text)) { it }
             )
         }
 
@@ -411,7 +411,7 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(100.dp)
-                .receiveContent(setOf(MediaType.Image)) {
+                .contentReceiver(setOf(MediaType.Image)) {
                     transferableContent = it
                     null // consume all
                 })
@@ -443,7 +443,7 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(100.dp)
-                .receiveContent(setOf(MediaType.Audio)) {
+                .contentReceiver(setOf(MediaType.Audio)) {
                     transferableContent = it
                     null // consume all
                 })
@@ -476,14 +476,14 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(200.dp)
-                .receiveContent(setOf(MediaType.All)) {
+                .contentReceiver(setOf(MediaType.All)) {
                     parentTransferableContent = it
                     null
                 }) {
                 Box(modifier = Modifier
                     .align(Alignment.Center)
                     .size(100.dp)
-                    .receiveContent(setOf(MediaType.Image)) {
+                    .contentReceiver(setOf(MediaType.Image)) {
                         childTransferableContent = it
                         it // don't consume anything
                     })
@@ -524,21 +524,21 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(200.dp)
-                .receiveContent(setOf(MediaType.All)) {
+                .contentReceiver(setOf(MediaType.All)) {
                     grandParentTransferableContent = it
                     null
                 }) {
                 Box(modifier = Modifier
                     .align(Alignment.Center)
                     .size(100.dp)
-                    .receiveContent(setOf(MediaType.Image)) {
+                    .contentReceiver(setOf(MediaType.Image)) {
                         parentTransferableContent = it
                         it // don't consume anything
                     }) {
                     Box(modifier = Modifier
                         .align(Alignment.Center)
                         .size(50.dp)
-                        .receiveContent(setOf(MediaType.Text)) {
+                        .contentReceiver(setOf(MediaType.Text)) {
                             childTransferableContent = it
                             it // don't consume anything
                         })
@@ -571,7 +571,7 @@
             Box(
                 modifier = Modifier
                     .size(100.dp)
-                    .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                    .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                         override fun onDragEnter() {
                             calls += "enter"
                         }
@@ -618,7 +618,7 @@
             Box(
                 modifier = Modifier
                     .size(200.dp)
-                    .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                    .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                         override fun onDragEnter() {
                             calls += "enter-1"
                         }
@@ -636,7 +636,7 @@
                     modifier = Modifier
                         .align(Alignment.Center)
                         .size(100.dp)
-                        .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                        .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                             override fun onDragEnter() {
                                 calls += "enter-2"
                             }
@@ -654,7 +654,7 @@
                         modifier = Modifier
                             .align(Alignment.Center)
                             .size(50.dp)
-                            .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                            .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                                 override fun onDragEnter() {
                                     calls += "enter-3"
                                 }
@@ -710,7 +710,7 @@
             Box(
                 modifier = Modifier
                     .size(100.dp)
-                    .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                    .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                         override fun onDragStart() {
                             calls += "start"
                         }
@@ -757,7 +757,7 @@
             Box(
                 modifier = Modifier
                     .size(200.dp)
-                    .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                    .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                         override fun onDragStart() {
                             calls += "start-1"
                         }
@@ -775,7 +775,7 @@
                     modifier = Modifier
                         .align(Alignment.Center)
                         .size(100.dp)
-                        .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                        .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                             override fun onDragStart() {
                                 calls += "start-2"
                             }
@@ -793,7 +793,7 @@
                         modifier = Modifier
                             .align(Alignment.Center)
                             .size(50.dp)
-                            .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                            .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                                 override fun onDragStart() {
                                     calls += "start-3"
                                 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TransferableContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TransferableContentTest.kt
index edcb824..d97f989 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TransferableContentTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TransferableContentTest.kt
@@ -90,14 +90,14 @@
     @Test
     fun consumeEach_returnsNull_ifEverythingIsConsumed() {
         val transferableContent = TransferableContent(createClipData())
-        val remaining = transferableContent.consumeEach { true }
+        val remaining = transferableContent.consume { true }
         assertThat(remaining).isNull()
     }
 
     @Test
     fun consumeEach_returnsSameObject_ifNothingIsConsumed() {
         val transferableContent = TransferableContent(createClipData())
-        val remaining = transferableContent.consumeEach { false }
+        val remaining = transferableContent.consume { false }
         assertThat(remaining).isSameInstanceAs(transferableContent)
     }
 
@@ -109,7 +109,7 @@
             addUri(mimeType = "image/gif")
         })
         // only text would remain
-        val remaining = transferableContent.consumeEach { it.uri != null }
+        val remaining = transferableContent.consume { it.uri != null }
         assertThat(remaining?.clipEntry?.clipData?.itemCount).isEqualTo(1)
         assertThat(remaining?.clipEntry?.clipData?.getItemAt(0)?.uri).isNull()
         assertThat(remaining?.hasMediaType(MediaType("video/mp4"))).isTrue()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingTest.kt
new file mode 100644
index 0000000..2f008d6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingTest.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.view.inputmethod.CursorAnchorInfo
+import android.view.inputmethod.ExtractedText
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.setFocusableContent
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.input.InputMethodInterceptor
+import androidx.compose.foundation.text.input.internal.InputMethodManager
+import androidx.compose.foundation.text.input.internal.inputMethodManagerFactory
+import androidx.compose.foundation.text.matchers.isZero
+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.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CoreTextFieldHandwritingTest {
+    @get:Rule
+    val rule = createComposeRule()
+    private val inputMethodInterceptor = InputMethodInterceptor(rule)
+
+    private val Tag = "CoreTextField"
+
+    private val fakeImm = object : InputMethodManager {
+        private var stylusHandwritingStartCount = 0
+
+        fun expectStylusHandwriting(started: Boolean) {
+            if (started) {
+                assertThat(stylusHandwritingStartCount).isEqualTo(1)
+                stylusHandwritingStartCount = 0
+            } else {
+                assertThat(stylusHandwritingStartCount).isZero()
+            }
+        }
+
+        override fun isActive(): Boolean = true
+
+        override fun restartInput() {}
+
+        override fun showSoftInput() {}
+
+        override fun hideSoftInput() {}
+
+        override fun updateExtractedText(token: Int, extractedText: ExtractedText) {}
+
+        override fun updateSelection(
+            selectionStart: Int,
+            selectionEnd: Int,
+            compositionStart: Int,
+            compositionEnd: Int
+        ) {}
+
+        override fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {}
+
+        override fun startStylusHandwriting() {
+            ++stylusHandwritingStartCount
+        }
+    }
+
+    @Before
+    fun setup() {
+        // Test is only meaningful when stylusHandwriting is supported.
+        assumeTrue(isStylusHandwritingSupported)
+    }
+
+    @Test
+    fun coreTextField_startHandwriting_unfocused() {
+        testStylusHandwriting(stylusHandwritingStarted = true) {
+            performStylusHandwriting()
+        }
+    }
+
+    @Test
+    fun coreTextField_startStylusHandwriting_unfocused() {
+        testStylusHandwriting(stylusHandwritingStarted = true) {
+            performStylusHandwriting()
+        }
+    }
+
+    @Test
+    fun coreTextField_startStylusHandwriting_focused() {
+        testStylusHandwriting(stylusHandwritingStarted = true) {
+            requestFocus()
+            performStylusHandwriting()
+        }
+    }
+
+    @Test
+    fun coreTextField_click_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusClick()
+        }
+    }
+
+    @Test
+    fun coreTextField_longClick_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusLongClick()
+        }
+    }
+
+    @Test
+    fun coreTextField_longPressAndDrag_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusLongPressAndDrag()
+        }
+    }
+
+    @Test
+    fun coreTextField_toggleEnabled_startStylusHandwriting() {
+        inputMethodManagerFactory = { fakeImm }
+
+        var enabled by mutableStateOf(true)
+
+        setContent {
+            val value = remember { TextFieldValue() }
+            CoreTextField(
+                value = value,
+                onValueChange = { },
+                modifier = Modifier
+                    .fillMaxSize()
+                    .testTag(Tag),
+                enabled = enabled
+            )
+        }
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+
+        // Toggle enabled to false, shouldn't start handwriting
+        enabled = false
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = false)
+
+        // Toggle to true again, should be able to start handwriting
+        enabled = true
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+    }
+
+    @Test
+    fun coreTextField_toggleReadOnly_startStylusHandwriting() {
+        inputMethodManagerFactory = { fakeImm }
+
+        var readOnly by mutableStateOf(false)
+
+        setContent {
+            val value = remember { TextFieldValue() }
+            CoreTextField(
+                value = value,
+                onValueChange = { },
+                modifier = Modifier
+                    .fillMaxSize()
+                    .testTag(Tag),
+                readOnly = readOnly
+            )
+        }
+
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+
+        // Toggle enabled to true, shouldn't start handwriting
+        readOnly = true
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = false)
+
+        // Toggle to true again, should be able to start handwriting
+        readOnly = false
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+    }
+
+    private fun testStylusHandwriting(
+        stylusHandwritingStarted: Boolean,
+        interaction: SemanticsNodeInteraction.() -> Unit
+    ) {
+        inputMethodManagerFactory = { fakeImm }
+
+        setContent {
+            val value = remember { TextFieldValue() }
+            CoreTextField(
+                value = value,
+                onValueChange = { },
+                modifier = Modifier
+                    .fillMaxSize()
+                    .testTag(Tag)
+            )
+        }
+
+        interaction.invoke(rule.onNodeWithTag(Tag))
+        rule.waitForIdle()
+        fakeImm.expectStylusHandwriting(stylusHandwritingStarted)
+    }
+
+    private fun setContent(
+        extraItemForInitialFocus: Boolean = true,
+        content: @Composable () -> Unit
+    ) {
+        rule.setFocusableContent(extraItemForInitialFocus) {
+            inputMethodInterceptor.Content {
+                content()
+            }
+        }
+    }
+
+    private fun performHandwritingAndExpect(stylusHandwritingStarted: Boolean) {
+        rule.onNodeWithTag(Tag).performStylusHandwriting()
+        rule.waitForIdle()
+        fakeImm.expectStylusHandwriting(stylusHandwritingStarted)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
index cc4fccf..e3f3e01 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
@@ -471,6 +471,7 @@
                 cursorAnchorInfos += cursorAnchorInfo
             }
 
+            override fun startStylusHandwriting() {}
             override fun isActive(): Boolean = true
             override fun restartInput() {}
             override fun showSoftInput() {}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
index 601de6e..ca78f4d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
@@ -18,11 +18,15 @@
 
 import android.os.Build
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.text.input.InputMethodInterceptor
+import androidx.compose.foundation.text.input.TestSoftwareKeyboardController
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusProperties
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -51,6 +55,8 @@
     @get:Rule
     val rule = createComposeRule()
 
+    private val inputMethodInterceptor = InputMethodInterceptor(rule)
+
     // We need to wrap the inline class parameter in another class because Java can't instantiate
     // the inline class.
     class Param(val imeAction: ImeAction) {
@@ -76,42 +82,45 @@
         val (value1, value2, value3) = List(3) { TextFieldValue("Placeholder Text") }
         val (textField1, textField2, textField3) = FocusRequester.createRefs()
         var (focusState1, focusState2, focusState3) = List(3) { false }
-        val keyboardHelper = KeyboardHelper(rule)
+        val keyboardController = TestSoftwareKeyboardController(rule)
 
-        rule.setContent {
-            keyboardHelper.initialize()
-            Column {
-                CoreTextField(
-                    value = value1,
-                    onValueChange = {},
-                    modifier = Modifier
-                        .focusRequester(textField1)
-                        .onFocusChanged { focusState1 = it.isFocused }
-                )
-                CoreTextField(
-                    value = value2,
-                    onValueChange = {},
-                    modifier = Modifier
-                        .testTag(initialTextField)
-                        .focusRequester(textField2)
-                        .focusProperties { previous = textField1; next = textField3 }
-                        .onFocusChanged { focusState2 = it.isFocused },
-                    imeOptions = ImeOptions(imeAction = imeAction)
-                )
-                CoreTextField(
-                    value = value3,
-                    onValueChange = {},
-                    modifier = Modifier
-                        .focusRequester(textField3)
-                        .onFocusChanged { focusState3 = it.isFocused }
-                )
+        inputMethodInterceptor.setContent {
+            CompositionLocalProvider(
+                LocalSoftwareKeyboardController provides keyboardController
+            ) {
+                Column {
+                    CoreTextField(
+                        value = value1,
+                        onValueChange = {},
+                        modifier = Modifier
+                            .focusRequester(textField1)
+                            .onFocusChanged { focusState1 = it.isFocused }
+                    )
+                    CoreTextField(
+                        value = value2,
+                        onValueChange = {},
+                        modifier = Modifier
+                            .testTag(initialTextField)
+                            .focusRequester(textField2)
+                            .focusProperties { previous = textField1; next = textField3 }
+                            .onFocusChanged { focusState2 = it.isFocused },
+                        imeOptions = ImeOptions(imeAction = imeAction)
+                    )
+                    CoreTextField(
+                        value = value3,
+                        onValueChange = {},
+                        modifier = Modifier
+                            .focusRequester(textField3)
+                            .onFocusChanged { focusState3 = it.isFocused }
+                    )
+                }
             }
         }
 
         // Show keyboard.
         rule.onNodeWithTag(initialTextField).performClick()
-        keyboardHelper.waitForKeyboardVisibility(visible = true)
-        assertThat(keyboardHelper.isSoftwareKeyboardShown()).isTrue()
+        inputMethodInterceptor.assertSessionActive()
+        keyboardController.show()
 
         // Act.
         rule.onNodeWithTag(initialTextField).performImeAction()
@@ -137,8 +146,7 @@
                 assertThat(focusState3).isFalse()
 
                 // Software keyboard is hidden.
-                keyboardHelper.waitForKeyboardVisibility(false)
-                assertThat(keyboardHelper.isSoftwareKeyboardShown()).isFalse()
+                keyboardController.assertHidden()
             }
             else -> {
                 // No change to focus state.
@@ -157,51 +165,53 @@
         val (value1, value2, value3) = List(3) { TextFieldValue("Placeholder Text") }
         val (textField1, textField2, textField3) = FocusRequester.createRefs()
         var (focusState1, focusState2, focusState3) = List(3) { false }
-        val keyboardHelper = KeyboardHelper(rule)
+        val keyboardController = TestSoftwareKeyboardController(rule)
 
-        rule.setContent {
-            keyboardHelper.initialize()
-            Column {
-                CoreTextField(
-                    value = value1,
-                    onValueChange = {},
-                    modifier = Modifier
-                        .focusRequester(textField1)
-                        .onFocusChanged { focusState1 = it.isFocused }
-                )
-                CoreTextField(
-                    value = value2,
-                    onValueChange = {},
-                    modifier = Modifier
-                        .testTag(initialTextField)
-                        .focusRequester(textField2)
-                        .focusProperties { previous = textField1; next = textField3 }
-                        .onFocusChanged { focusState2 = it.isFocused },
-                    imeOptions = ImeOptions(imeAction = imeAction),
-                    keyboardActions = KeyboardActions(
-                        onDone = { defaultKeyboardAction(Done) },
-                        onGo = { defaultKeyboardAction(Go) },
-                        onNext = { defaultKeyboardAction(Next) },
-                        onPrevious = { defaultKeyboardAction(Previous) },
-                        onSearch = { defaultKeyboardAction(Search) },
-                        onSend = { defaultKeyboardAction(Send) },
+        inputMethodInterceptor.setContent {
+            CompositionLocalProvider(
+                LocalSoftwareKeyboardController provides keyboardController
+            ) {
+                Column {
+                    CoreTextField(
+                        value = value1,
+                        onValueChange = {},
+                        modifier = Modifier
+                            .focusRequester(textField1)
+                            .onFocusChanged { focusState1 = it.isFocused }
                     )
-                )
-                CoreTextField(
-                    value = value3,
-                    onValueChange = {},
-                    modifier = Modifier
-                        .focusRequester(textField3)
-                        .onFocusChanged { focusState3 = it.isFocused }
-                )
+                    CoreTextField(
+                        value = value2,
+                        onValueChange = {},
+                        modifier = Modifier
+                            .testTag(initialTextField)
+                            .focusRequester(textField2)
+                            .focusProperties { previous = textField1; next = textField3 }
+                            .onFocusChanged { focusState2 = it.isFocused },
+                        imeOptions = ImeOptions(imeAction = imeAction),
+                        keyboardActions = KeyboardActions(
+                            onDone = { defaultKeyboardAction(Done) },
+                            onGo = { defaultKeyboardAction(Go) },
+                            onNext = { defaultKeyboardAction(Next) },
+                            onPrevious = { defaultKeyboardAction(Previous) },
+                            onSearch = { defaultKeyboardAction(Search) },
+                            onSend = { defaultKeyboardAction(Send) },
+                        )
+                    )
+                    CoreTextField(
+                        value = value3,
+                        onValueChange = {},
+                        modifier = Modifier
+                            .focusRequester(textField3)
+                            .onFocusChanged { focusState3 = it.isFocused }
+                    )
+                }
             }
         }
 
         // Show keyboard.
         rule.onNodeWithTag(initialTextField).performClick()
-
-        keyboardHelper.waitForKeyboardVisibility(visible = true)
-        assertThat(keyboardHelper.isSoftwareKeyboardShown()).isTrue()
+        inputMethodInterceptor.assertSessionActive()
+        keyboardController.show()
 
         // Act.
         rule.onNodeWithTag(initialTextField).performImeAction()
@@ -227,8 +237,7 @@
                 assertThat(focusState3).isFalse()
 
                 // Software keyboard is hidden.
-                keyboardHelper.waitForKeyboardVisibility(false)
-                assertThat(keyboardHelper.isSoftwareKeyboardShown()).isFalse()
+                keyboardController.assertHidden()
             }
             else -> {
                 // No change to focus state.
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
new file mode 100644
index 0000000..966c702
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.foundation.text
+
+import android.view.KeyEvent
+import android.view.MotionEvent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.invokeGlobalAssertions
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.center
+import androidx.compose.ui.unit.toOffset
+import androidx.core.view.InputDeviceCompat
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.math.roundToInt
+
+// We don't have StylusInjectionScope at the moment. This is a simplified implementation for
+// the basic use cases in this test. It only supports single stylus pointer, and the pointerId
+// is totally ignored.
+internal class HandwritingTestStylusInjectScope(
+    semanticsNode: SemanticsNode
+) : TouchInjectionScope, Density by semanticsNode.layoutInfo.density {
+    private val root = semanticsNode.root as ViewRootForTest
+    private val downTime: Long = System.currentTimeMillis()
+
+    private var lastPosition: Offset = Offset.Unspecified
+    private var currentTime: Long = System.currentTimeMillis()
+    private val boundsInRoot = semanticsNode.boundsInRoot
+
+    override val visibleSize: IntSize =
+        IntSize(boundsInRoot.width.roundToInt(), boundsInRoot.height.roundToInt())
+
+    override val viewConfiguration: ViewConfiguration =
+        semanticsNode.layoutInfo.viewConfiguration
+
+    private fun localToRoot(position: Offset): Offset {
+        return position + boundsInRoot.topLeft
+    }
+
+    override fun advanceEventTime(durationMillis: Long) {
+        require(durationMillis >= 0) {
+            "duration of a delay can only be positive, not $durationMillis"
+        }
+        currentTime += durationMillis
+    }
+
+    override fun currentPosition(pointerId: Int): Offset? {
+        return lastPosition
+    }
+
+    override fun down(pointerId: Int, position: Offset) {
+        val rootPosition = localToRoot(position)
+        lastPosition = rootPosition
+        sendTouchEvent(KeyEvent.ACTION_DOWN)
+    }
+
+    override fun updatePointerTo(pointerId: Int, position: Offset) {
+        lastPosition = localToRoot(position)
+    }
+
+    override fun move(delayMillis: Long) {
+        advanceEventTime(delayMillis)
+        sendTouchEvent(MotionEvent.ACTION_MOVE)
+    }
+
+    @ExperimentalTestApi
+    override fun moveWithHistoryMultiPointer(
+        relativeHistoricalTimes: List<Long>,
+        historicalCoordinates: List<List<Offset>>,
+        delayMillis: Long
+    ) {
+        // Not needed for this test because Android only support one stylus pointer.
+    }
+
+    override fun up(pointerId: Int) {
+        sendTouchEvent(MotionEvent.ACTION_UP)
+    }
+
+    override fun cancel(delayMillis: Long) {
+        sendTouchEvent(MotionEvent.ACTION_CANCEL)
+    }
+
+    private fun sendTouchEvent(action: Int) {
+        val positionInScreen = run {
+            val array = intArrayOf(0, 0)
+            root.view.getLocationOnScreen(array)
+            Offset(array[0].toFloat(), array[1].toFloat())
+        }
+        val motionEvent = MotionEvent.obtain(
+            /* downTime = */ downTime,
+            /* eventTime = */ currentTime,
+            /* action = */ action,
+            /* pointerCount = */ 1,
+            /* pointerProperties = */ arrayOf(
+                MotionEvent.PointerProperties().apply {
+                    id = 0
+                    toolType = MotionEvent.TOOL_TYPE_STYLUS
+                }
+            ),
+            /* pointerCoords = */ arrayOf(
+                MotionEvent.PointerCoords().apply {
+                    val startOffset = lastPosition
+
+                    // Allows for non-valid numbers/Offsets to be passed along to Compose to
+                    // test if it handles them properly (versus breaking here and we not knowing
+                    // if Compose properly handles these values).
+                    x = if (startOffset.isValid()) {
+                        positionInScreen.x + startOffset.x
+                    } else {
+                        Float.NaN
+                    }
+
+                    y = if (startOffset.isValid()) {
+                        positionInScreen.y + startOffset.y
+                    } else {
+                        Float.NaN
+                    }
+                }
+            ),
+            /* metaState = */ 0,
+            /* buttonState = */ 0,
+            /* xPrecision = */ 1f,
+            /* yPrecision = */ 1f,
+            /* deviceId = */ 0,
+            /* edgeFlags = */ 0,
+            /* source = */ InputDeviceCompat.SOURCE_TOUCHSCREEN,
+            /* flags = */ 0
+        )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            root.view.dispatchTouchEvent(motionEvent)
+        }
+    }
+}
+
+/** Start stylus handwriting on the target element. */
+internal fun SemanticsNodeInteraction.performStylusHandwriting() {
+    performStylusInput {
+        val startPosition = visibleSize.center.toOffset()
+        down(startPosition)
+        moveTo(startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f))
+        up()
+    }
+}
+
+internal fun SemanticsNodeInteraction.performStylusClick() {
+    performStylusInput {
+        down(visibleSize.center.toOffset())
+        move()
+        up()
+    }
+}
+
+internal fun SemanticsNodeInteraction.performStylusLongClick() {
+    performStylusInput {
+        down(visibleSize.center.toOffset())
+        move(viewConfiguration.longPressTimeoutMillis + 1)
+        up()
+    }
+}
+
+internal fun SemanticsNodeInteraction.performStylusLongPressAndDrag() {
+    performStylusInput {
+        val startPosition = visibleSize.center.toOffset()
+        down(visibleSize.center.toOffset())
+        val position = startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f)
+        moveTo(
+            position = position,
+            delayMillis = viewConfiguration.longPressTimeoutMillis + 1
+        )
+        up()
+    }
+}
+
+private fun SemanticsNodeInteraction.performStylusInput(
+    block: TouchInjectionScope.() -> Unit
+): SemanticsNodeInteraction {
+    @OptIn(ExperimentalTestApi::class)
+    invokeGlobalAssertions()
+    val node = fetchSemanticsNode("Failed to inject stylus input.")
+    val stylusInjectionScope = HandwritingTestStylusInjectScope(node)
+    block.invoke(stylusInjectionScope)
+    return this
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt
index 0053b63..0e6f520 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt
@@ -16,41 +16,33 @@
 
 package androidx.compose.foundation.text.input
 
-import android.view.KeyEvent.ACTION_DOWN
-import android.view.MotionEvent
-import android.view.MotionEvent.ACTION_CANCEL
-import android.view.MotionEvent.ACTION_MOVE
-import android.view.MotionEvent.ACTION_UP
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.performStylusClick
+import androidx.compose.foundation.text.performStylusHandwriting
+import androidx.compose.foundation.text.performStylusLongClick
+import androidx.compose.foundation.text.performStylusLongPressAndDrag
+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.geometry.Offset
-import androidx.compose.ui.platform.ViewConfiguration
-import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.TouchInjectionScope
-import androidx.compose.ui.test.invokeGlobalAssertions
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.requestFocus
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.center
-import androidx.compose.ui.unit.toOffset
-import androidx.core.view.InputDeviceCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import kotlin.math.roundToInt
+import org.junit.Assume.assumeTrue
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
+@OptIn(ExperimentalFoundationApi::class)
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 internal class BasicTextFieldHandwritingTest {
@@ -66,6 +58,12 @@
 
     private val imm = FakeInputMethodManager()
 
+    @Before
+    fun setup() {
+        // Test is only meaningful when stylus handwriting is supported.
+        assumeTrue(isStylusHandwritingSupported)
+    }
+
     @Test
     fun textField_startStylusHandwriting_unfocused() {
         testStylusHandwriting(stylusHandwritingStarted = true) {
@@ -84,38 +82,21 @@
     @Test
     fun textField_click_notStartStylusHandwriting() {
         testStylusHandwriting(stylusHandwritingStarted = false) {
-            performStylusInput {
-                down(visibleSize.center.toOffset())
-                move()
-                up()
-            }
+            performStylusClick()
         }
     }
 
     @Test
     fun textField_longClick_notStartStylusHandwriting() {
         testStylusHandwriting(stylusHandwritingStarted = false) {
-            performStylusInput {
-                down(visibleSize.center.toOffset())
-                move(viewConfiguration.longPressTimeoutMillis + 1)
-                up()
-            }
+            performStylusLongClick()
         }
     }
 
     @Test
     fun textField_longPressAndDrag_notStartStylusHandwriting() {
         testStylusHandwriting(stylusHandwritingStarted = false) {
-            performStylusInput {
-                val startPosition = visibleSize.center.toOffset()
-                down(visibleSize.center.toOffset())
-                val position = startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f)
-                moveTo(
-                    position = position,
-                    delayMillis = viewConfiguration.longPressTimeoutMillis + 1
-                )
-                up()
-            }
+            performStylusLongPressAndDrag()
         }
     }
 
@@ -157,6 +138,58 @@
         }
     }
 
+    @Test
+    fun textField_toggleEnabled_startStylusHandwriting() {
+        immRule.setFactory { imm }
+        var enabled by mutableStateOf(true)
+        inputMethodInterceptor.setTextFieldTestContent {
+            val state = remember { TextFieldState() }
+            BasicTextField(
+                state = state,
+                modifier = Modifier.fillMaxSize().testTag(Tag),
+                enabled = enabled
+            )
+        }
+
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+
+        // Toggle enabled to false, shouldn't start handwriting
+        enabled = false
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = false)
+
+        // Toggle to true again, should be able to start handwriting
+        enabled = true
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+    }
+
+    @Test
+    fun textField_toggleReadOnly_startStylusHandwriting() {
+        immRule.setFactory { imm }
+        var readOnly by mutableStateOf(false)
+        inputMethodInterceptor.setTextFieldTestContent {
+            val state = remember { TextFieldState() }
+            BasicTextField(
+                state = state,
+                modifier = Modifier.fillMaxSize().testTag(Tag),
+                readOnly = readOnly
+            )
+        }
+
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+
+        // Toggle enabled to true, shouldn't start handwriting
+        readOnly = true
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = false)
+
+        // Toggle to true again, should be able to start handwriting
+        readOnly = false
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+    }
+
     private fun testStylusHandwriting(
         stylusHandwritingStarted: Boolean,
         interaction: SemanticsNodeInteraction.() -> Unit
@@ -180,142 +213,14 @@
         }
     }
 
-    /** Start stylus handwriting on the target element. */
-    private fun SemanticsNodeInteraction.performStylusHandwriting() {
-        performStylusInput {
-            val startPosition = visibleSize.center.toOffset()
-            down(startPosition)
-            moveTo(startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f))
-            up()
-        }
-    }
-
-    private fun SemanticsNodeInteraction.performStylusInput(
-        block: TouchInjectionScope.() -> Unit
-    ): SemanticsNodeInteraction {
-        @OptIn(ExperimentalTestApi::class)
-        invokeGlobalAssertions()
-        val node = fetchSemanticsNode("Failed to inject stylus input.")
-        val stylusInjectionScope = StylusInjectionScope(node)
-        block.invoke(stylusInjectionScope)
-        return this
-    }
-
-    // We don't have StylusInjectionScope at the moment. This is a simplified implementation for
-    // the basic use cases in this test. It only supports single stylus pointer, and the pointerId
-    // is totally ignored.
-    private inner class StylusInjectionScope(
-        semanticsNode: SemanticsNode
-    ) : TouchInjectionScope, Density by semanticsNode.layoutInfo.density {
-        private val root = semanticsNode.root as ViewRootForTest
-        private val downTime: Long = System.currentTimeMillis()
-
-        private var lastPosition: Offset = Offset.Unspecified
-        private var currentTime: Long = System.currentTimeMillis()
-        private val boundsInRoot = semanticsNode.boundsInRoot
-
-        override val visibleSize: IntSize =
-            IntSize(boundsInRoot.width.roundToInt(), boundsInRoot.height.roundToInt())
-
-        override val viewConfiguration: ViewConfiguration =
-            semanticsNode.layoutInfo.viewConfiguration
-
-        private fun localToRoot(position: Offset): Offset {
-            return position + boundsInRoot.topLeft
-        }
-
-        override fun advanceEventTime(durationMillis: Long) {
-            require(durationMillis >= 0) {
-                "duration of a delay can only be positive, not $durationMillis"
-            }
-            currentTime += durationMillis
-        }
-
-        override fun currentPosition(pointerId: Int): Offset? {
-            return lastPosition
-        }
-
-        override fun down(pointerId: Int, position: Offset) {
-            val rootPosition = localToRoot(position)
-            lastPosition = rootPosition
-            sendTouchEvent(ACTION_DOWN)
-        }
-
-        override fun updatePointerTo(pointerId: Int, position: Offset) {
-            lastPosition = localToRoot(position)
-        }
-
-        override fun move(delayMillis: Long) {
-            advanceEventTime(delayMillis)
-            sendTouchEvent(ACTION_MOVE)
-        }
-
-        @ExperimentalTestApi
-        override fun moveWithHistoryMultiPointer(
-            relativeHistoricalTimes: List<Long>,
-            historicalCoordinates: List<List<Offset>>,
-            delayMillis: Long
-        ) {
-            // Not needed for this test because Android only support one stylus pointer.
-        }
-
-        override fun up(pointerId: Int) {
-            sendTouchEvent(ACTION_UP)
-        }
-
-        override fun cancel(delayMillis: Long) {
-            sendTouchEvent(ACTION_CANCEL)
-        }
-
-        private fun sendTouchEvent(action: Int) {
-            val positionInScreen = run {
-                val array = intArrayOf(0, 0)
-                root.view.getLocationOnScreen(array)
-                Offset(array[0].toFloat(), array[1].toFloat())
-            }
-            val motionEvent = MotionEvent.obtain(
-                /* downTime = */ downTime,
-                /* eventTime = */ currentTime,
-                /* action = */ action,
-                /* pointerCount = */ 1,
-                /* pointerProperties = */ arrayOf(
-                    MotionEvent.PointerProperties().apply {
-                        id = 0
-                        toolType = MotionEvent.TOOL_TYPE_STYLUS
-                    }
-                ),
-                /* pointerCoords = */ arrayOf(
-                    MotionEvent.PointerCoords().apply {
-                        val startOffset = lastPosition
-
-                        // Allows for non-valid numbers/Offsets to be passed along to Compose to
-                        // test if it handles them properly (versus breaking here and we not knowing
-                        // if Compose properly handles these values).
-                        x = if (startOffset.isValid()) {
-                            positionInScreen.x + startOffset.x
-                        } else {
-                            Float.NaN
-                        }
-
-                        y = if (startOffset.isValid()) {
-                            positionInScreen.y + startOffset.y
-                        } else {
-                            Float.NaN
-                        }
-                    }
-                ),
-                /* metaState = */ 0,
-                /* buttonState = */ 0,
-                /* xPrecision = */ 1f,
-                /* yPrecision = */ 1f,
-                /* deviceId = */ 0,
-                /* edgeFlags = */ 0,
-                /* source = */ InputDeviceCompat.SOURCE_TOUCHSCREEN,
-                /* flags = */ 0
-            )
-
-            rule.runOnUiThread {
-                root.view.dispatchTouchEvent(motionEvent)
+    private fun performHandwritingAndExpect(stylusHandwritingStarted: Boolean) {
+        rule.onNodeWithTag(Tag).performStylusHandwriting()
+        rule.waitForIdle()
+        rule.runOnIdle {
+            if (stylusHandwritingStarted) {
+                imm.expectCall("startStylusHandwriting")
+            } else {
+                imm.expectNoMoreCalls()
             }
         }
     }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
index 3c3f61d..90f6621 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
@@ -24,9 +24,9 @@
 import androidx.compose.foundation.content.MediaType
 import androidx.compose.foundation.content.ReceiveContentListener
 import androidx.compose.foundation.content.TransferableContent
-import androidx.compose.foundation.content.consumeEach
+import androidx.compose.foundation.content.consume
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.createClipData
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.content.testDragAndDrop
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.collectIsHoveredAsState
@@ -87,7 +87,7 @@
     @Test
     fun nonTextContent_isAcceptedIfReceiveContentDefined() {
         rule.setContentAndTestDragAndDrop(
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 null
             }
         ) {
@@ -120,7 +120,7 @@
     @Test
     fun draggingNonText_updatesSelection_withReceiveContent() {
         rule.setContentAndTestDragAndDrop(
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 null
             }
         ) {
@@ -225,7 +225,7 @@
                 Box(
                     modifier = Modifier
                         .size(200.dp)
-                        .receiveContent(emptySet(), object : ReceiveContentListener {
+                        .contentReceiver(emptySet(), object : ReceiveContentListener {
                             override fun onDragStart() {
                                 calls += "start"
                             }
@@ -298,7 +298,7 @@
                 Box(
                     modifier = Modifier
                         .size(200.dp)
-                        .receiveContent(emptySet(), object : ReceiveContentListener {
+                        .contentReceiver(emptySet(), object : ReceiveContentListener {
                             override fun onDragStart() {
                                 calls += "start"
                             }
@@ -364,9 +364,9 @@
         lateinit var receivedContent: TransferableContent
         rule.setContentAndTestDragAndDrop(
             "Hello World!",
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 receivedContent = it
-                receivedContent.consumeEach {
+                receivedContent.consume {
                     // do not consume text
                     it.uri != null
                 }
@@ -390,7 +390,7 @@
         lateinit var receivedContent: TransferableContent
         rule.setContentAndTestDragAndDrop(
             "Hello World!",
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 receivedContent = it
                 // consume everything
                 null
@@ -414,7 +414,7 @@
         lateinit var receivedContent: TransferableContent
         rule.setContentAndTestDragAndDrop(
             "Hello World!",
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 receivedContent = it
                 val uri = receivedContent.clipEntry.firstUriOrNull()
                 // replace the content
@@ -436,7 +436,7 @@
     fun droppedItem_requestsPermission_ifReceiveContent() {
         rule.setContentAndTestDragAndDrop(
             "Hello World!",
-            modifier = Modifier.receiveContent(emptySet()) { null }
+            modifier = Modifier.contentReceiver(emptySet()) { null }
         ) {
             drag(Offset(fontSize.toPx() * 5, 10f), defaultUri)
             drop()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldReceiveContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldReceiveContentTest.kt
index 695108a..7539f76 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldReceiveContentTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldReceiveContentTest.kt
@@ -26,9 +26,9 @@
 import androidx.compose.foundation.content.MediaType
 import androidx.compose.foundation.content.TransferableContent
 import androidx.compose.foundation.content.assertClipData
-import androidx.compose.foundation.content.consumeEach
+import androidx.compose.foundation.content.consume
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.createClipData
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.draganddrop.dragAndDropTarget
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.text.BasicTextField
@@ -124,7 +124,7 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.Image)) { null }
+                    .contentReceiver(setOf(MediaType.Image)) { null }
             )
         }
         rule.onNodeWithTag(tag).requestFocus()
@@ -141,7 +141,7 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(
+                    .contentReceiver(
                         setOf(
                             MediaType.Image,
                             MediaType.PlainText,
@@ -167,12 +167,12 @@
     @Test
     fun multiReceiveContent_mergesMediaTypes() {
         inputMethodInterceptor.setContent {
-            Box(modifier = Modifier.receiveContent(setOf(MediaType.Text)) { null }) {
+            Box(modifier = Modifier.contentReceiver(setOf(MediaType.Text)) { null }) {
                 BasicTextField(
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) { null }
+                        .contentReceiver(setOf(MediaType.Image)) { null }
                 )
             }
         }
@@ -191,14 +191,14 @@
     @Test
     fun multiReceiveContent_mergesMediaTypes_uniquely() {
         inputMethodInterceptor.setContent {
-            Box(modifier = Modifier.receiveContent(
+            Box(modifier = Modifier.contentReceiver(
                 setOf(MediaType.Text, MediaType.Image)
             ) { null }) {
                 BasicTextField(
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) { null }
+                        .contentReceiver(setOf(MediaType.Image)) { null }
                 )
             }
         }
@@ -218,7 +218,7 @@
     fun multiReceiveContent_mergesMediaTypes_includingAnotherTraversableNode() {
         inputMethodInterceptor.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Text)) { null }
+                .contentReceiver(setOf(MediaType.Text)) { null }
                 .dragAndDropTarget({ true }, object : DragAndDropTarget {
                     override fun onDrop(event: DragAndDropEvent): Boolean {
                         return false
@@ -229,7 +229,7 @@
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) { null }
+                        .contentReceiver(setOf(MediaType.Image)) { null }
                 )
             }
         }
@@ -253,7 +253,7 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         transferableContent = it
                         null
                     }
@@ -299,7 +299,7 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         transferableContent = it
                         null
                     }
@@ -338,11 +338,11 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         parentTransferableContent = it
                         null
                     }
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         childTransferableContent = it
                         it
                     }
@@ -383,11 +383,11 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         parentTransferableContent = it
                         null
                     }
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         childTransferableContent = it
                         null
                     }
@@ -422,7 +422,7 @@
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) {
+                        .contentReceiver(setOf(MediaType.Image)) {
                             transferableContent = it
                             null
                         }
@@ -455,8 +455,8 @@
                     state = state,
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image, MediaType.Text)) {
-                            it.consumeEach { item ->
+                        .contentReceiver(setOf(MediaType.Image, MediaType.Text)) {
+                            it.consume { item ->
                                 // only consume if there's no text
                                 item.text == null
                             }
@@ -494,21 +494,21 @@
                     state = state,
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Text)) {
+                        .contentReceiver(setOf(MediaType.Text)) {
                             transferableContent1 = it
-                            it.consumeEach {
+                            it.consume {
                                 it.text.contains("a")
                             }
                         }
-                        .receiveContent(setOf(MediaType.Text)) {
+                        .contentReceiver(setOf(MediaType.Text)) {
                             transferableContent2 = it
-                            it.consumeEach {
+                            it.consume {
                                 it.text.contains("b")
                             }
                         }
-                        .receiveContent(setOf(MediaType.Text)) {
+                        .contentReceiver(setOf(MediaType.Text)) {
                             transferableContent3 = it
-                            it.consumeEach {
+                            it.consume {
                                 it.text.contains("c")
                             }
                         }
@@ -548,7 +548,7 @@
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) {
+                        .contentReceiver(setOf(MediaType.Image)) {
                             transferableContent = it
                             null
                         }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
index 0443ce1..8781298 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
@@ -755,6 +755,32 @@
         }
     }
 
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun textFieldDoesNotCrash_inVerticallyScrollableContainer_whenFieldShrinks() {
+        // Start as a single line, then enter '\n' to grow to 2 lines.
+        val state = TextFieldState("\n\n\n\n\n\n\n\n\n")
+        rule.setContent {
+            BasicTextField(
+                state,
+                // The field should never scroll internally.
+                lineLimits = MultiLine(maxHeightInLines = Int.MAX_VALUE),
+                modifier = Modifier
+                    .testTag("field")
+                    .border(1.dp, Color.Blue)
+            )
+        }
+        rule.onNodeWithTag("field").requestFocus()
+
+        // remove lines in quick succession and expect to not crash
+        repeat(state.text.length) {
+            rule.onNodeWithTag("field").performKeyInput { pressKey(Key.Backspace) }
+        }
+
+        rule.waitForIdle()
+        assertThat(state.text.toString()).isEmpty()
+    }
+
     private fun ComposeContentTestRule.setupHorizontallyScrollableContent(
         state: TextFieldState,
         scrollState: ScrollState,
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
index 1e4b8ec..0106d69 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
@@ -18,8 +18,8 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.content.MediaType
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.createClipData
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -582,7 +582,7 @@
             toolbar = textToolbar,
             singleLine = true,
             clipboardManager = clipboardManager,
-            modifier = Modifier.receiveContent(setOf(MediaType.Image)) { null }
+            modifier = Modifier.contentReceiver(setOf(MediaType.Image)) { null }
         )
 
         rule.onNodeWithTag(TAG).performTouchInput { click() }
@@ -986,7 +986,7 @@
         }
     }
 
-    override fun setClip(clipEntry: ClipEntry) {
+    override fun setClip(clipEntry: ClipEntry?) {
         if (supportsClipEntry) {
             currentClipEntry = clipEntry
         } else {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/TransferableContent.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/TransferableContent.android.kt
index 7c0f682..8ac71ce5 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/TransferableContent.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/TransferableContent.android.kt
@@ -28,8 +28,8 @@
 /**
  * Android specific parts of [TransferableContent].
  *
- * @param linkUri Only supplied by InputConnection#commitContent.
- * @param extras Extras bundle that's passed by InputConnection#commitContent.
+ * @property linkUri Only supplied by InputConnection#commitContent.
+ * @property extras Extras bundle that's passed by InputConnection#commitContent.
  */
 @ExperimentalFoundationApi
 actual class PlatformTransferableContent internal constructor(
@@ -61,16 +61,18 @@
 
 /**
  * Helper function to consume parts of [TransferableContent] in Android by splitting it to
- * [ClipData.Item] parts. Use this function in [receiveContent] modifier's `onReceive` callback to
+ * [ClipData.Item] parts. Use this function in [contentReceiver] modifier's `onReceive` callback to
  * easily separate remaining parts from incoming [TransferableContent].
  *
+ * @sample androidx.compose.foundation.samples.ReceiveContentBasicSample
+ *
  * @param predicate Decides whether to consume or leave the given item out. Return true to indicate
  * that this particular item was processed here, it shouldn't be passed further down the content
  * receiver chain. Return false to keep it in the returned [TransferableContent].
  * @return Remaining parts of this [TransferableContent].
  */
 @ExperimentalFoundationApi
-fun TransferableContent.consumeEach(predicate: (ClipData.Item) -> Boolean): TransferableContent? {
+fun TransferableContent.consume(predicate: (ClipData.Item) -> Boolean): TransferableContent? {
     val clipData = clipEntry.clipData
     return if (clipData.itemCount == 1) {
         // return this if the single item inside ClipData is not consumed, or null if it's consumed
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.android.kt
similarity index 74%
rename from room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
rename to compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.android.kt
index cc75983..3ba3d83 100644
--- a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.android.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.room.migration.bundle
-// empty file to trigger klib creation
-// see: https://youtrack.jetbrains.com/issue/KT-52344
+package androidx.compose.foundation.text.handwriting
+
+import android.os.Build
+
+internal actual val isStylusHandwritingSupported: Boolean =
+    Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/InputMethodManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/InputMethodManager.android.kt
index e5298d9..9aaa89a 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/InputMethodManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/InputMethodManager.android.kt
@@ -17,10 +17,13 @@
 package androidx.compose.foundation.text.input.internal
 
 import android.content.Context
+import android.os.Build
 import android.util.Log
 import android.view.View
 import android.view.inputmethod.CursorAnchorInfo
 import android.view.inputmethod.ExtractedText
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
 import androidx.core.view.SoftwareKeyboardControllerCompat
 
 internal interface InputMethodManager {
@@ -45,6 +48,8 @@
     )
 
     fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo)
+
+    fun startStylusHandwriting()
 }
 
 /**
@@ -98,4 +103,18 @@
     override fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {
         imm.updateCursorAnchorInfo(view, cursorAnchorInfo)
     }
+
+    override fun startStylusHandwriting() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            Api34StartStylusHandwriting.startStylusHandwriting(imm, view)
+        }
+    }
+}
+
+@RequiresApi(34)
+internal object Api34StartStylusHandwriting {
+    @DoNotInline
+    fun startStylusHandwriting(imm: android.view.inputmethod.InputMethodManager, view: View) {
+        imm.startStylusHandwriting(view)
+    }
 }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.android.kt
index 8cd284c..067d170 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.android.kt
@@ -23,6 +23,7 @@
 import android.view.inputmethod.BaseInputConnection
 import android.view.inputmethod.EditorInfo
 import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.platform.PlatformTextInputMethodRequest
@@ -36,7 +37,14 @@
 import androidx.emoji2.text.EmojiCompat
 import java.lang.ref.WeakReference
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
 
 private const val DEBUG_CLASS = "AndroidLegacyPlatformTextInputServiceAdapter"
 
@@ -55,6 +63,21 @@
 
     private var job: Job? = null
     private var currentRequest: LegacyTextInputMethodRequest? = null
+    private var backingStylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
+    private var stylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
+        get() {
+            val finalStylusHandwritingTrigger = backingStylusHandwritingTrigger
+            if (finalStylusHandwritingTrigger != null) {
+                return finalStylusHandwritingTrigger
+            }
+            if (!isStylusHandwritingSupported) {
+                return null
+            }
+            return MutableSharedFlow<Unit>(
+                replay = 1,
+                onBufferOverflow = BufferOverflow.DROP_LATEST
+            ).also { backingStylusHandwritingTrigger = it }
+        }
 
     override fun startInput(
         value: TextFieldValue,
@@ -89,27 +112,40 @@
         // No need to cancel any previous job, the text input system ensures the previous session
         // will be cancelled.
         job = node.launchTextInputSession {
-            val request = LegacyTextInputMethodRequest(
-                view = view,
-                localToScreen = ::localToScreen,
-                inputMethodManager = inputMethodManagerFactory(view)
-            )
-            initializeRequest?.invoke(request)
-            currentRequest = request
-            try {
-                startInputMethod(request)
-            } finally {
-                currentRequest = null
+            coroutineScope {
+                val inputMethodManager = inputMethodManagerFactory(view)
+                val request = LegacyTextInputMethodRequest(
+                    view = view,
+                    localToScreen = ::localToScreen,
+                    inputMethodManager = inputMethodManager
+                )
+
+                if (isStylusHandwritingSupported) {
+                    launch(start = CoroutineStart.UNDISPATCHED) {
+                        stylusHandwritingTrigger?.collect {
+                            inputMethodManager.startStylusHandwriting()
+                        }
+                    }
+                }
+                initializeRequest?.invoke(request)
+                currentRequest = request
+                try {
+                    startInputMethod(request)
+                } finally {
+                    currentRequest = null
+                }
             }
         }
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     override fun stopInput() {
         if (DEBUG) {
             Log.d(TAG, "$DEBUG_CLASS.stopInput")
         }
         job?.cancel()
         job = null
+        stylusHandwritingTrigger?.resetReplayCache()
     }
 
     override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
@@ -136,6 +172,14 @@
             decorationBoxBounds
         )
     }
+
+    /**
+     * Signal the InputMethodManager to startStylusHandwriting. This method can be called
+     * after the editor calls startInput or just before the editor calls startInput.
+     */
+    override fun startStylusHandwriting() {
+        stylusHandwritingTrigger?.tryEmit(Unit)
+    }
 }
 
 /**
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
index 457a4a6..5aab181 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
@@ -26,6 +26,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.job
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
@@ -64,6 +65,47 @@
     }
 
     @Test
+    fun edit_doesNotAllow_reentrantBehavior() {
+        assertFailsWith<IllegalStateException>(
+            "TextFieldState does not support concurrent or nested editing."
+        ) {
+            state.edit {
+                replace(0, 0, "hello")
+                state.edit {
+                    replace(0, 0, "hello")
+                }
+            }
+        }
+        assertThat(state.text.toString()).isEmpty()
+    }
+
+    @Test
+    fun edit_doesNotAllow_concurrentAccess() {
+        assertFailsWith<IllegalStateException>(
+            "TextFieldState does not support concurrent or nested editing."
+        ) {
+            runTest {
+                var edit2Started = false
+                launch {
+                    state.edit {
+                        replace(0, 0, "hello")
+                        while (!edit2Started) delay(10)
+                    }
+                }
+                launch {
+                    state.edit {
+                        edit2Started = true
+                        replace(0, 0, "hello")
+                    }
+                }
+                advanceUntilIdle()
+                runCurrent()
+            }
+        }
+        assertThat(state.text.toString()).isEmpty()
+    }
+
+    @Test
     fun edit_doesNotChange_whenThrows() {
         class ExpectedException : RuntimeException()
 
@@ -78,6 +120,24 @@
     }
 
     @Test
+    fun edit_canEditAgain_ifFirstOneThrows() {
+        class ExpectedException : RuntimeException()
+
+        assertFailsWith<ExpectedException> {
+            state.edit {
+                replace(0, 0, "hello")
+                throw ExpectedException()
+            }
+        }
+        assertThat(state.text.toString()).isEmpty()
+
+        state.edit {
+            replace(0, 0, "hello")
+        }
+        assertThat(state.text.toString()).isEqualTo("hello")
+    }
+
+    @Test
     fun edit_invalidates_whenSelectionChanged() = runTestWithSnapshotsThenCancelChildren {
         val text = "hello"
         val state = TextFieldState(text, initialSelection = TextRange(0))
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContent.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContent.kt
index 90d2762..55cf28a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContent.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContent.kt
@@ -42,47 +42,15 @@
  * supports. It's possible that this modifier receives other type of content that's not specified in
  * this set. Please make sure to check again whether the received [TransferableContent] carries a
  * supported [MediaType]. An empty [MediaType] set implies [MediaType.All].
- * @param onReceive Callback that's triggered when a content is successfully committed. Return
- * an optional [TransferableContent] that contains the unprocessed or unaccepted parts of the
- * received [TransferableContent]. The remaining [TransferableContent] first will be sent to to the
- * closest ancestor [receiveContent] modifier. This chain will continue until there's no ancestor
- * modifier left, or [TransferableContent] is fully consumed. After, the source subsystem that
- * created the original [TransferableContent] and initiated the chain will receive any remaining
- * items to execute its default behavior. For example a text editor that receives content should
- * insert any remaining text to the drop position.
- *
- * @sample androidx.compose.foundation.samples.ReceiveContentBasicSample
- */
-@ExperimentalFoundationApi
-fun Modifier.receiveContent(
-    hintMediaTypes: Set<MediaType>,
-    onReceive: (TransferableContent) -> TransferableContent?
-): Modifier = then(
-    ReceiveContentElement(
-        hintMediaTypes = hintMediaTypes,
-        receiveContentListener = ReceiveContentListener(onReceive)
-    )
-)
-
-/**
- * Configures the current node and any children nodes as a Content Receiver.
- *
- * Content in this context refers to a [TransferableContent] that could be received from another
- * app through Drag-and-Drop, Copy/Paste, or from the Software Keyboard.
- *
- * @param hintMediaTypes A set of media types that are expected by this receiver. This set
- * gets passed to the Software Keyboard to send information about what type of content the editor
- * supports. It's possible that this modifier receives other type of content that's not specified in
- * this set. Please make sure to check again whether the received [TransferableContent] carries a
- * supported [MediaType]. An empty [MediaType] set implies [MediaType.All].
- * @param receiveContentListener A set of callbacks that includes certain Drag-and-Drop state
- * changes. Please checkout [ReceiveContentListener] docs for an explanation of each callback.
+ * @param receiveContentListener Listener to respond to the receive event. This interface also
+ * includes a set of callbacks for certain Drag-and-Drop state changes. Please checkout
+ * [ReceiveContentListener] docs for an explanation of each callback.
  *
  * @sample androidx.compose.foundation.samples.ReceiveContentFullSample
  */
 @Suppress("ExecutorRegistration")
 @ExperimentalFoundationApi
-fun Modifier.receiveContent(
+fun Modifier.contentReceiver(
     hintMediaTypes: Set<MediaType>,
     receiveContentListener: ReceiveContentListener
 ): Modifier = then(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContentListener.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContentListener.kt
index 40934de..d5fc700 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContentListener.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContentListener.kt
@@ -20,14 +20,14 @@
 import androidx.compose.foundation.draganddrop.dragAndDropTarget
 
 /**
- * A set of callbacks for [receiveContent] modifier to get information about certain Drag-and-Drop
+ * A set of callbacks for [contentReceiver] modifier to get information about certain Drag-and-Drop
  * state changes, as well as receiving the payload carrying [TransferableContent].
  *
- * [receiveContent]'s drop target supports nesting. When two [receiveContent] modifiers are nested
+ * [contentReceiver]'s drop target supports nesting. When two [contentReceiver] modifiers are nested
  * on the composition tree, parent's drop target actually includes child's bounds, meaning that
  * they are not mutually exclusive like the regular [dragAndDropTarget].
  *
- * Let's assume we have two [receiveContent] boxes named A and B where B is a child of A, aligned
+ * Let's assume we have two [contentReceiver] boxes named A and B where B is a child of A, aligned
  * to bottom end.
  *
  * ---------
@@ -51,22 +51,22 @@
  *
  * The interesting part in this order of calls is that A does not receive an exit event when the
  * item moves over to B. This is different than what would happen if you were to use
- * [dragAndDropTarget] modifier because semantically [receiveContent] works as a chain of nodes.
+ * [dragAndDropTarget] modifier because semantically [contentReceiver] works as a chain of nodes.
  * If the item were to be dropped on B, its [onReceive] chain would also call A's [onReceive] with
  * what's left from B.
  */
 @ExperimentalFoundationApi
-interface ReceiveContentListener {
+fun interface ReceiveContentListener {
 
     /**
-     * Optional callback that's called when a dragging session starts. All [receiveContent] nodes
+     * Optional callback that's called when a dragging session starts. All [contentReceiver] nodes
      * in the current composition tree receives this callback immediately.
      */
     fun onDragStart() = Unit
 
     /**
      * Optional callback that's called when a dragging session ends by either successful drop, or
-     * cancellation. All [receiveContent] nodes in the current composition tree receives this
+     * cancellation. All [contentReceiver] nodes in the current composition tree receives this
      * callback immediately.
      */
     fun onDragEnd() = Unit
@@ -83,9 +83,9 @@
 
     /**
      * Callback that's triggered when a content is successfully committed.
-     * Return an optional [TransferableContent] that contains the ignored parts of the received
+     * @return An optional [TransferableContent] that contains the ignored parts of the received
      * [TransferableContent] by this node. The remaining [TransferableContent] first will be sent to
-     * to the closest ancestor [receiveContent] modifier. This chain will continue until there's no
+     * to the closest ancestor [contentReceiver] modifier. This chain will continue until there's no
      * ancestor modifier left, or [TransferableContent] is fully consumed. After, the source
      * subsystem that created the original [TransferableContent] and initiated the chain will
      * receive any remaining items to apply its default behavior. For example a text editor that
@@ -94,15 +94,3 @@
      */
     fun onReceive(transferableContent: TransferableContent): TransferableContent?
 }
-
-@OptIn(ExperimentalFoundationApi::class)
-internal fun ReceiveContentListener(
-    onReceive: (TransferableContent) -> TransferableContent?
-): ReceiveContentListener {
-    val paramOnReceive = onReceive
-    return object : ReceiveContentListener {
-        override fun onReceive(transferableContent: TransferableContent): TransferableContent? {
-            return paramOnReceive(transferableContent)
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/TransferableContent.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/TransferableContent.kt
index 00fb943..bc812cb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/TransferableContent.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/TransferableContent.kt
@@ -25,13 +25,13 @@
  *
  * Note; Consult platform-specific guidelines for best practices in content transfer operations.
  *
- * @param clipEntry The main content data, typically representing a text, image, file, or other
+ * @property clipEntry The main content data, typically representing a text, image, file, or other
  * transferable item.
- * @param source The source from which the content originated like Keyboard, DragAndDrop, or
+ * @property source The source from which the content originated like Keyboard, DragAndDrop, or
  * Clipboard.
- * @param clipMetadata Metadata associated with the content, providing additional information or
+ * @property clipMetadata Metadata associated with the content, providing additional information or
  * context.
- * @param platformTransferableContent Optional platform-specific representation of the content, or
+ * @property platformTransferableContent Optional platform-specific representation of the content, or
  * additional platform-specific information, that can be used to access platform level APIs.
  */
 @ExperimentalFoundationApi
@@ -51,10 +51,21 @@
 
         companion object {
 
+            /**
+             * Indicates that the [TransferableContent] originates from the soft keyboard (also
+             * known as input method editor or IME)
+             */
             val Keyboard = Source(0)
 
+            /**
+             * Indicates that the [TransferableContent] was passed on by the system drag and drop.
+             */
             val DragAndDrop = Source(1)
 
+            /**
+             * Indicates that the [TransferableContent] comes from the clipboard via paste.
+             * (e.g. "Paste" action in the floating action menu or "Ctrl+V" key combination)
+             */
             val Clipboard = Source(2)
         }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentConfiguration.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentConfiguration.kt
index dbe906e..9cd4d0d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentConfiguration.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentConfiguration.kt
@@ -23,7 +23,7 @@
 import androidx.compose.foundation.content.ReceiveContentListener
 import androidx.compose.foundation.content.ReceiveContentNode
 import androidx.compose.foundation.content.TransferableContent
-import androidx.compose.foundation.content.receiveContent
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.ui.modifier.ModifierLocalModifierNode
 import androidx.compose.ui.modifier.modifierLocalOf
 
@@ -122,8 +122,8 @@
         }
 
     /**
-     * A getter that returns the closest [receiveContent] modifier configuration if this node is
-     * attached. It returns null if the node is detached or there is no parent [receiveContent]
+     * A getter that returns the closest [contentReceiver] modifier configuration if this node is
+     * attached. It returns null if the node is detached or there is no parent [contentReceiver]
      * found.
      */
     private fun getParentReceiveContentListener(): ReceiveContentListener? {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TargetedFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TargetedFlingBehavior.kt
index a63253a..388b65a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TargetedFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TargetedFlingBehavior.kt
@@ -37,9 +37,10 @@
      *
      * @param initialVelocity velocity available for fling in the orientation specified in
      * [androidx.compose.foundation.gestures.scrollable] that invoked this method.
-     * @param onRemainingDistanceUpdated a lambda that will be called anytime the
-     * distance to the settling offset is updated. The settling offset is the final offset where
-     * this fling will stop and may change depending on the snapping animation progression.
+     * @param onRemainingDistanceUpdated a lambda that will be called anytime the distance to the
+     * settling offset is updated. The settling offset in pixels is passed to this lambda an it
+     * represents the final offset where this fling will stop and may change depending on the
+     * snapping animation progression.
      *
      * @return remaining velocity after fling operation has ended
      */
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
index 4b4394b..169b1c5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
@@ -19,6 +19,8 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
+import androidx.compose.foundation.lazy.layout.estimatedLazyMaxScrollOffset
+import androidx.compose.foundation.lazy.layout.estimatedLazyScrollOffset
 import androidx.compose.ui.semantics.CollectionInfo
 
 internal fun LazyLayoutSemanticState(
@@ -26,12 +28,17 @@
     isVertical: Boolean
 ): LazyLayoutSemanticState = object : LazyLayoutSemanticState {
 
-    override val firstVisibleItemScrollOffset: Int
-        get() = state.firstVisibleItemScrollOffset
-    override val firstVisibleItemIndex: Int
-        get() = state.firstVisibleItemIndex
-    override val canScrollForward: Boolean
-        get() = state.canScrollForward
+    override val scrollOffset: Float
+        get() = estimatedLazyScrollOffset(
+            state.firstVisibleItemIndex,
+            state.firstVisibleItemScrollOffset
+        )
+    override val maxScrollOffset: Float
+        get() = estimatedLazyMaxScrollOffset(
+            state.firstVisibleItemIndex,
+            state.firstVisibleItemScrollOffset,
+            state.canScrollForward
+        )
 
     override suspend fun animateScrollBy(delta: Float) {
         state.animateScrollBy(delta)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index 9bdd775..2c691af 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -298,41 +298,43 @@
             state.scrollDeltaBetweenPasses
         }
 
-        measureLazyList(
-            itemsCount = itemsCount,
-            measuredItemProvider = measuredItemProvider,
-            mainAxisAvailableSize = mainAxisAvailableSize,
-            beforeContentPadding = beforeContentPadding,
-            afterContentPadding = afterContentPadding,
-            spaceBetweenItems = spaceBetweenItems,
-            firstVisibleItemIndex = firstVisibleItemIndex,
-            firstVisibleItemScrollOffset = firstVisibleScrollOffset,
-            scrollToBeConsumed = scrollToBeConsumed,
-            constraints = contentConstraints,
-            isVertical = isVertical,
-            headerIndexes = itemProvider.headerIndexes,
-            verticalArrangement = verticalArrangement,
-            horizontalArrangement = horizontalArrangement,
-            reverseLayout = reverseLayout,
-            density = this,
-            itemAnimator = state.itemAnimator,
-            beyondBoundsItemCount = beyondBoundsItemCount,
-            pinnedItems = pinnedItems,
-            hasLookaheadPassOccurred = hasLookaheadPassOccurred,
-            isLookingAhead = isLookingAhead,
-            postLookaheadLayoutInfo = state.postLookaheadLayoutInfo,
-            coroutineScope = coroutineScope,
-            placementScopeInvalidator = state.placementScopeInvalidator,
-            layout = { width, height, placement ->
-                layout(
-                    containerConstraints.constrainWidth(width + totalHorizontalPadding),
-                    containerConstraints.constrainHeight(height + totalVerticalPadding),
-                    emptyMap(),
-                    placement
-                )
-            }
-        ).also {
-            state.applyMeasureResult(it, isLookingAhead)
+        val measureResult = Snapshot.withMutableSnapshot {
+            measureLazyList(
+                itemsCount = itemsCount,
+                measuredItemProvider = measuredItemProvider,
+                mainAxisAvailableSize = mainAxisAvailableSize,
+                beforeContentPadding = beforeContentPadding,
+                afterContentPadding = afterContentPadding,
+                spaceBetweenItems = spaceBetweenItems,
+                firstVisibleItemIndex = firstVisibleItemIndex,
+                firstVisibleItemScrollOffset = firstVisibleScrollOffset,
+                scrollToBeConsumed = scrollToBeConsumed,
+                constraints = contentConstraints,
+                isVertical = isVertical,
+                headerIndexes = itemProvider.headerIndexes,
+                verticalArrangement = verticalArrangement,
+                horizontalArrangement = horizontalArrangement,
+                reverseLayout = reverseLayout,
+                density = this,
+                itemAnimator = state.itemAnimator,
+                beyondBoundsItemCount = beyondBoundsItemCount,
+                pinnedItems = pinnedItems,
+                hasLookaheadPassOccurred = hasLookaheadPassOccurred,
+                isLookingAhead = isLookingAhead,
+                postLookaheadLayoutInfo = state.postLookaheadLayoutInfo,
+                coroutineScope = coroutineScope,
+                placementScopeInvalidator = state.placementScopeInvalidator,
+                layout = { width, height, placement ->
+                    layout(
+                        containerConstraints.constrainWidth(width + totalHorizontalPadding),
+                        containerConstraints.constrainHeight(height + totalVerticalPadding),
+                        emptyMap(),
+                        placement
+                    )
+                }
+            )
         }
+        state.applyMeasureResult(measureResult, isLookingAhead)
+        measureResult
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index feff117..332f12c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -321,39 +321,41 @@
             state.beyondBoundsInfo
         )
 
-        measureLazyGrid(
-            itemsCount = itemsCount,
-            measuredLineProvider = measuredLineProvider,
-            measuredItemProvider = measuredItemProvider,
-            mainAxisAvailableSize = mainAxisAvailableSize,
-            beforeContentPadding = beforeContentPadding,
-            afterContentPadding = afterContentPadding,
-            spaceBetweenLines = spaceBetweenLines,
-            firstVisibleLineIndex = firstVisibleLineIndex,
-            firstVisibleLineScrollOffset = firstVisibleLineScrollOffset,
-            scrollToBeConsumed = state.scrollToBeConsumed,
-            constraints = contentConstraints,
-            isVertical = isVertical,
-            verticalArrangement = verticalArrangement,
-            horizontalArrangement = horizontalArrangement,
-            reverseLayout = reverseLayout,
-            density = this,
-            placementAnimator = state.placementAnimator,
-            spanLayoutProvider = spanLayoutProvider,
-            pinnedItems = pinnedItems,
-            coroutineScope = coroutineScope,
-            placementScopeInvalidator = state.placementScopeInvalidator,
-            prefetchInfoRetriever = prefetchInfoRetriever,
-            layout = { width, height, placement ->
-                layout(
-                    containerConstraints.constrainWidth(width + totalHorizontalPadding),
-                    containerConstraints.constrainHeight(height + totalVerticalPadding),
-                    emptyMap(),
-                    placement
-                )
-            }
-        ).also {
-            state.applyMeasureResult(it)
+        val measureResult = Snapshot.withMutableSnapshot {
+            measureLazyGrid(
+                itemsCount = itemsCount,
+                measuredLineProvider = measuredLineProvider,
+                measuredItemProvider = measuredItemProvider,
+                mainAxisAvailableSize = mainAxisAvailableSize,
+                beforeContentPadding = beforeContentPadding,
+                afterContentPadding = afterContentPadding,
+                spaceBetweenLines = spaceBetweenLines,
+                firstVisibleLineIndex = firstVisibleLineIndex,
+                firstVisibleLineScrollOffset = firstVisibleLineScrollOffset,
+                scrollToBeConsumed = state.scrollToBeConsumed,
+                constraints = contentConstraints,
+                isVertical = isVertical,
+                verticalArrangement = verticalArrangement,
+                horizontalArrangement = horizontalArrangement,
+                reverseLayout = reverseLayout,
+                density = this,
+                placementAnimator = state.placementAnimator,
+                spanLayoutProvider = spanLayoutProvider,
+                pinnedItems = pinnedItems,
+                coroutineScope = coroutineScope,
+                placementScopeInvalidator = state.placementScopeInvalidator,
+                prefetchInfoRetriever = prefetchInfoRetriever,
+                layout = { width, height, placement ->
+                    layout(
+                        containerConstraints.constrainWidth(width + totalHorizontalPadding),
+                        containerConstraints.constrainHeight(height + totalVerticalPadding),
+                        emptyMap(),
+                        placement
+                    )
+                }
+            )
         }
+        state.applyMeasureResult(measureResult)
+        measureResult
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
index 834e3da..208f87c4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
@@ -20,6 +20,8 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
+import androidx.compose.foundation.lazy.layout.estimatedLazyMaxScrollOffset
+import androidx.compose.foundation.lazy.layout.estimatedLazyScrollOffset
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.semantics.CollectionInfo
@@ -32,12 +34,17 @@
 ): LazyLayoutSemanticState =
     remember(state, reverseScrolling) {
         object : LazyLayoutSemanticState {
-            override val firstVisibleItemScrollOffset: Int
-                get() = state.firstVisibleItemScrollOffset
-            override val firstVisibleItemIndex: Int
-                get() = state.firstVisibleItemIndex
-            override val canScrollForward: Boolean
-                get() = state.canScrollForward
+            override val scrollOffset: Float
+                get() = estimatedLazyScrollOffset(
+                    state.firstVisibleItemIndex,
+                    state.firstVisibleItemScrollOffset
+                )
+            override val maxScrollOffset: Float
+                get() = estimatedLazyMaxScrollOffset(
+                    state.firstVisibleItemIndex,
+                    state.firstVisibleItemScrollOffset,
+                    state.canScrollForward
+                )
 
             override suspend fun animateScrollBy(delta: Float) {
                 state.animateScrollBy(delta)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
index 3a08b20..f1d49fd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
@@ -21,7 +21,6 @@
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.SubcomposeLayout
@@ -111,15 +110,13 @@
             modifier,
             remember(itemContentFactory, measurePolicy) {
                 { constraints ->
-                    Snapshot.withMutableSnapshot {
-                        with(
-                            LazyLayoutMeasureScopeImpl(
-                                itemContentFactory,
-                                this
-                            )
-                        ) {
-                            measurePolicy(constraints)
-                        }
+                    with(
+                        LazyLayoutMeasureScopeImpl(
+                            itemContentFactory,
+                            this
+                        )
+                    ) {
+                        measurePolicy(constraints)
                     }
                 }
             }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
index e77a308..ce101ad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
@@ -198,8 +198,8 @@
 
     private fun updateCachedSemanticsValues() {
         scrollAxisRange = ScrollAxisRange(
-            value = { state.pseudoScrollOffset() },
-            maxValue = { state.pseudoMaxScrollOffset() },
+            value = { state.scrollOffset },
+            maxValue = { state.maxScrollOffset },
             reverseScrolling = reverseScrolling
         )
 
@@ -236,41 +236,49 @@
 }
 
 internal interface LazyLayoutSemanticState {
-    val firstVisibleItemScrollOffset: Int
-    val firstVisibleItemIndex: Int
-    val canScrollForward: Boolean
     val viewport: Int
     val contentPadding: Int
+    val scrollOffset: Float
+    val maxScrollOffset: Float
 
     fun collectionInfo(): CollectionInfo
     suspend fun animateScrollBy(delta: Float)
     suspend fun scrollToItem(index: Int)
+}
 
-    // It is impossible for lazy lists to provide an absolute scroll offset because the size of the
-    // items above the viewport is not known, but the AccessibilityEvent system API expects one
-    // anyway. So this provides a best-effort pseudo-offset that avoids breaking existing behavior.
-    //
-    // The key properties that A11y services are known to actually rely on are:
-    // A) each scroll change generates a TYPE_VIEW_SCROLLED AccessibilityEvent
-    // B) the integer offset in the AccessibilityEvent is different than the last one (note that the
-    // magnitude and direction of the change does not matter for the known use cases)
-    // C) scrollability is indicated by whether the scroll position is exactly 0 or exactly
-    // maxScrollOffset
-    //
-    // To preserve property B) as much as possible, the constant 500 is chosen to be larger than a
-    // single scroll delta would realistically be, while small enough to avoid losing precision due
-    // to the 24-bit float significand of ScrollAxisRange with realistic list sizes (if there are
-    // fewer than ~16000 items, the integer value is exactly preserved).
-    fun pseudoScrollOffset() =
-        (firstVisibleItemScrollOffset + firstVisibleItemIndex * 500).toFloat()
+// It is impossible for lazy lists to provide an absolute scroll offset because the size of the
+// items above the viewport is not known, but the AccessibilityEvent system API expects one
+// anyway. So this provides a best-effort pseudo-offset that avoids breaking existing behavior.
+//
+// The key properties that A11y services are known to actually rely on are:
+// A) each scroll change generates a TYPE_VIEW_SCROLLED AccessibilityEvent
+// B) the integer offset in the AccessibilityEvent is different than the last one (note that the
+// magnitude and direction of the change does not matter for the known use cases)
+// C) scrollability is indicated by whether the scroll position is exactly 0 or exactly
+// maxScrollOffset
+//
+// To preserve property B) as much as possible, the constant 500 is chosen to be larger than a
+// single scroll delta would realistically be, while small enough to avoid losing precision due
+// to the 24-bit float significand of ScrollAxisRange with realistic list sizes (if there are
+// fewer than ~16000 items, the integer value is exactly preserved).
+internal fun estimatedLazyScrollOffset(
+    firstVisibleItemIndex: Int,
+    firstVisibleItemScrollOffset: Int
+): Float {
+    return (firstVisibleItemScrollOffset + firstVisibleItemIndex * 500).toFloat()
+}
 
-    fun pseudoMaxScrollOffset() =
-        if (canScrollForward) {
-            // If we can scroll further, indicate that by setting it slightly higher than
-            // the current value
-            pseudoScrollOffset() + 100
-        } else {
-            // If we can't scroll further, the current value is the max
-            pseudoScrollOffset()
-        }.toFloat()
+internal fun estimatedLazyMaxScrollOffset(
+    firstVisibleItemIndex: Int,
+    firstVisibleItemScrollOffset: Int,
+    canScrollForward: Boolean
+): Float {
+    return if (canScrollForward) {
+        // If we can scroll further, indicate that by setting it slightly higher than
+        // the current value
+        estimatedLazyScrollOffset(firstVisibleItemIndex, firstVisibleItemScrollOffset) + 100
+    } else {
+        // If we can't scroll further, the current value is the max
+        estimatedLazyScrollOffset(firstVisibleItemIndex, firstVisibleItemScrollOffset)
+    }.toFloat()
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
index 1488567..1590e42 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.lazy.layout.calculateLazyLayoutPinnedIndices
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
@@ -96,26 +97,28 @@
             state.beyondBoundsInfo
         )
 
-        measureStaggeredGrid(
-            state = state,
-            pinnedItems = pinnedItems,
-            itemProvider = itemProvider,
-            resolvedSlots = resolvedSlots,
-            constraints = constraints.copy(
-                minWidth = constraints.constrainWidth(horizontalPadding),
-                minHeight = constraints.constrainHeight(verticalPadding)
-            ),
-            mainAxisSpacing = mainAxisSpacing.roundToPx(),
-            contentOffset = contentOffset,
-            mainAxisAvailableSize = mainAxisAvailableSize,
-            isVertical = isVertical,
-            reverseLayout = reverseLayout,
-            beforeContentPadding = beforeContentPadding,
-            afterContentPadding = afterContentPadding,
-            coroutineScope = coroutineScope
-        ).also {
-            state.applyMeasureResult(it)
+        val measureResult = Snapshot.withMutableSnapshot {
+            measureStaggeredGrid(
+                state = state,
+                pinnedItems = pinnedItems,
+                itemProvider = itemProvider,
+                resolvedSlots = resolvedSlots,
+                constraints = constraints.copy(
+                    minWidth = constraints.constrainWidth(horizontalPadding),
+                    minHeight = constraints.constrainHeight(verticalPadding)
+                ),
+                mainAxisSpacing = mainAxisSpacing.roundToPx(),
+                contentOffset = contentOffset,
+                mainAxisAvailableSize = mainAxisAvailableSize,
+                isVertical = isVertical,
+                reverseLayout = reverseLayout,
+                beforeContentPadding = beforeContentPadding,
+                afterContentPadding = afterContentPadding,
+                coroutineScope = coroutineScope
+            )
         }
+        state.applyMeasureResult(measureResult)
+        measureResult
     }
 }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemantics.kt
index 009f1e1..1d551a5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemantics.kt
@@ -20,6 +20,8 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
+import androidx.compose.foundation.lazy.layout.estimatedLazyMaxScrollOffset
+import androidx.compose.foundation.lazy.layout.estimatedLazyScrollOffset
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.semantics.CollectionInfo
@@ -32,12 +34,17 @@
 ): LazyLayoutSemanticState =
     remember(state, reverseScrolling) {
         object : LazyLayoutSemanticState {
-            override val firstVisibleItemScrollOffset: Int
-                get() = state.firstVisibleItemScrollOffset
-            override val firstVisibleItemIndex: Int
-                get() = state.firstVisibleItemIndex
-            override val canScrollForward: Boolean
-                get() = state.canScrollForward
+            override val scrollOffset: Float
+                get() = estimatedLazyScrollOffset(
+                    state.firstVisibleItemIndex,
+                    state.firstVisibleItemScrollOffset
+                )
+            override val maxScrollOffset: Float
+                get() = estimatedLazyMaxScrollOffset(
+                    state.firstVisibleItemIndex,
+                    state.firstVisibleItemScrollOffset,
+                    state.canScrollForward
+                )
 
             override suspend fun animateScrollBy(delta: Float) {
                 state.animateScrollBy(delta)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
index e4de927..97f97a6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
@@ -127,7 +127,6 @@
 
     val semanticState = rememberPagerSemanticState(
         state,
-        reverseLayout,
         orientation == Orientation.Vertical
     )
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutSemanticState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutSemanticState.kt
index 2d93543..e91f380 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutSemanticState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutSemanticState.kt
@@ -16,23 +16,19 @@
 
 package androidx.compose.foundation.pager
 
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
 import androidx.compose.ui.semantics.CollectionInfo
 
-@OptIn(ExperimentalFoundationApi::class)
 internal fun LazyLayoutSemanticState(
     state: PagerState,
     isVertical: Boolean
 ): LazyLayoutSemanticState = object : LazyLayoutSemanticState {
-    override val firstVisibleItemScrollOffset: Int
-        get() = state.firstVisiblePageOffset
-    override val firstVisibleItemIndex: Int
-        get() = state.firstVisiblePage
-    override val canScrollForward: Boolean
-        get() = state.canScrollForward
+    override val scrollOffset: Float
+        get() = state.currentAbsoluteScrollOffset().toFloat()
+    override val maxScrollOffset: Float
+        get() = state.layoutInfo.calculateNewMaxScrollOffset(state.pageCount).toFloat()
 
     override suspend fun animateScrollBy(delta: Float) {
         state.animateScrollBy(delta)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index 26dce70..50208f4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -22,7 +22,6 @@
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.FlingBehavior
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.TargetedFlingBehavior
@@ -287,7 +286,6 @@
      * position. If the velocity is high enough, the Pager will use the logic described in
      * [decayAnimationSpec] and [snapAnimationSpec].
      */
-    @OptIn(ExperimentalFoundationApi::class)
     @Composable
     fun flingBehavior(
         state: PagerState,
@@ -391,14 +389,6 @@
         }
     }
 
-    fun Offset.consumeOnOrientation(orientation: Orientation): Offset {
-        return if (orientation == Orientation.Vertical) {
-            copy(x = 0f)
-        } else {
-            copy(y = 0f)
-        }
-    }
-
     override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
         return if (
         // rounding error and drag only
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerBeyondBoundsModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerBeyondBoundsModifier.kt
index e1589eb..eb0103c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerBeyondBoundsModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerBeyondBoundsModifier.kt
@@ -15,12 +15,10 @@
  */
 package androidx.compose.foundation.pager
 
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal fun rememberPagerBeyondBoundsState(
     state: PagerState,
@@ -31,7 +29,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 internal class PagerBeyondBoundsState(
     private val state: PagerState,
     private val outOfBoundsPageCount: Int
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
index e708661..882afd4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
@@ -163,36 +163,38 @@
             beyondBoundsInfo = state.beyondBoundsInfo
         )
 
-        measurePager(
-            beforeContentPadding = beforeContentPadding,
-            afterContentPadding = afterContentPadding,
-            constraints = contentConstraints,
-            pageCount = pageCount(),
-            spaceBetweenPages = spaceBetweenPages,
-            mainAxisAvailableSize = mainAxisAvailableSize,
-            visualPageOffset = visualItemOffset,
-            pageAvailableSize = pageAvailableSize,
-            outOfBoundsPageCount = outOfBoundsPageCount,
-            orientation = orientation,
-            currentPage = currentPage,
-            currentPageOffset = currentPageOffset,
-            horizontalAlignment = horizontalAlignment,
-            verticalAlignment = verticalAlignment,
-            pagerItemProvider = itemProvider,
-            reverseLayout = reverseLayout,
-            pinnedPages = pinnedPages,
-            snapPosition = snapPosition,
-            placementScopeInvalidator = state.placementScopeInvalidator,
-            layout = { width, height, placement ->
-                layout(
-                    containerConstraints.constrainWidth(width + totalHorizontalPadding),
-                    containerConstraints.constrainHeight(height + totalVerticalPadding),
-                    emptyMap(),
-                    placement
-                )
-            }
-        ).also {
-            state.applyMeasureResult(it)
+        val measureResult = Snapshot.withMutableSnapshot {
+            measurePager(
+                beforeContentPadding = beforeContentPadding,
+                afterContentPadding = afterContentPadding,
+                constraints = contentConstraints,
+                pageCount = pageCount(),
+                spaceBetweenPages = spaceBetweenPages,
+                mainAxisAvailableSize = mainAxisAvailableSize,
+                visualPageOffset = visualItemOffset,
+                pageAvailableSize = pageAvailableSize,
+                outOfBoundsPageCount = outOfBoundsPageCount,
+                orientation = orientation,
+                currentPage = currentPage,
+                currentPageOffset = currentPageOffset,
+                horizontalAlignment = horizontalAlignment,
+                verticalAlignment = verticalAlignment,
+                pagerItemProvider = itemProvider,
+                reverseLayout = reverseLayout,
+                pinnedPages = pinnedPages,
+                snapPosition = snapPosition,
+                placementScopeInvalidator = state.placementScopeInvalidator,
+                layout = { width, height, placement ->
+                    layout(
+                        containerConstraints.constrainWidth(width + totalHorizontalPadding),
+                        containerConstraints.constrainHeight(height + totalVerticalPadding),
+                        emptyMap(),
+                        placement
+                    )
+                }
+            )
         }
+        state.applyMeasureResult(measureResult)
+        measureResult
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt
index 5a027a1..c182417 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt
@@ -110,12 +110,6 @@
         currentPageOffsetFraction = offsetFraction
     }
 
-    fun currentAbsoluteScrollOffset(): Long {
-        val currentPageOffset = currentPage.toLong() * state.pageSizeWithSpacing
-        val offsetFraction = (currentPageOffsetFraction * state.pageSizeWithSpacing).roundToLong()
-        return currentPageOffset + offsetFraction
-    }
-
     fun applyScrollDelta(delta: Int) {
         debugLog { "Applying Delta=$delta" }
         val fractionUpdate = if (state.pageSizeWithSpacing == 0) {
@@ -143,3 +137,9 @@
         println("PagerScrollPosition: ${generateMsg()}")
     }
 }
+
+internal fun PagerState.currentAbsoluteScrollOffset(): Long {
+    val currentPageOffset = currentPage.toLong() * pageSizeWithSpacing
+    val offsetFraction = (currentPageOffsetFraction * pageSizeWithSpacing).roundToLong()
+    return currentPageOffset + offsetFraction
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt
index 4647096..073834d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt
@@ -16,19 +16,16 @@
 
 package androidx.compose.foundation.pager
 
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal fun rememberPagerSemanticState(
     state: PagerState,
-    reverseScrolling: Boolean,
     isVertical: Boolean
 ): LazyLayoutSemanticState {
-    return remember(state, reverseScrolling, isVertical) {
+    return remember(state, isVertical) {
         LazyLayoutSemanticState(state, isVertical)
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index cce7c8f..1535b73 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -171,7 +171,7 @@
     internal var upDownDifference: Offset by mutableStateOf(Offset.Zero)
     private val animatedScrollScope = PagerLazyAnimateScrollScope(this)
 
-    internal val scrollPosition = PagerScrollPosition(currentPage, currentPageOffsetFraction, this)
+    private val scrollPosition = PagerScrollPosition(currentPage, currentPageOffsetFraction, this)
 
     internal var firstVisiblePage = currentPage
         private set
@@ -180,10 +180,8 @@
         private set
 
     private var maxScrollOffset: Long = Long.MAX_VALUE
-        private set
 
     private var minScrollOffset: Long = 0L
-        private set
 
     private var accumulator: Float = 0.0f
 
@@ -204,7 +202,7 @@
      * determine scroll deltas and max min scrolling.
      */
     private fun performScroll(delta: Float): Float {
-        val currentScrollPosition = scrollPosition.currentAbsoluteScrollOffset()
+        val currentScrollPosition = currentAbsoluteScrollOffset()
         debugLog {
             "\nDelta=$delta " +
                 "\ncurrentScrollPosition=$currentScrollPosition " +
@@ -276,8 +274,7 @@
     internal var layoutWithMeasurement: Int = 0
         private set
 
-    internal var layoutWithoutMeasurement: Int = 0
-        private set
+    private var layoutWithoutMeasurement: Int = 0
 
     /**
      * Only used for testing to disable prefetching when needed to test the main logic.
@@ -776,12 +773,10 @@
     ): Int = scrollPosition.matchPageWithKey(itemProvider, currentPage)
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 internal suspend fun PagerState.animateToNextPage() {
     if (currentPage + 1 < pageCount) animateScrollToPage(currentPage + 1)
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 internal suspend fun PagerState.animateToPreviousPage() {
     if (currentPage - 1 >= 0) animateScrollToPage(currentPage - 1)
 }
@@ -830,7 +825,7 @@
     }
 }
 
-private fun PagerMeasureResult.calculateNewMaxScrollOffset(pageCount: Int): Long {
+internal fun PagerLayoutInfo.calculateNewMaxScrollOffset(pageCount: Int): Long {
     val pageSizeWithSpacing = pageSpacing + pageSize
     val maxScrollPossible =
         (pageCount.toLong()) * pageSizeWithSpacing + beforeContentPadding + afterContentPadding
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 b7938b5..07cae9a 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,8 @@
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.bringIntoViewRequester
+import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
 import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -401,6 +403,29 @@
             textDragObserver = manager.touchSelectionObserver,
         )
         .pointerHoverIcon(textPointerIcon)
+        .then(
+            if (isStylusHandwritingSupported) {
+                Modifier.pointerInput(enabled, readOnly) {
+                    if (enabled && !readOnly) {
+                        detectStylusHandwriting {
+                            if (!state.hasFocus) {
+                                focusRequester.requestFocus()
+                            }
+                            // TextInputService is calling LegacyTextInputServiceAdapter under the
+                            // hood.  And because it's a public API, startStylusHandwriting is added
+                            // to legacyTextInputServiceAdapter instead.
+                            // startStylusHandwriting may be called before the actual input
+                            // session starts when the editor is not focused, this is handled
+                            // internally by the LegacyTextInputServiceAdapter.
+                            legacyTextInputServiceAdapter.startStylusHandwriting()
+                            true
+                        }
+                    }
+                }
+            } else {
+                Modifier
+            }
+        )
 
     val drawModifier = Modifier.drawBehind {
         state.layoutResult?.let { layoutResult ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
new file mode 100644
index 0000000..3d4ba3b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.handwriting
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.util.fastFirstOrNull
+
+/**
+ * A utility function that detects stylus movements and calls the [onHandwritingSlopExceeded] when
+ * it detects that stylus movement has exceeds the handwriting slop.
+ * If [onHandwritingSlopExceeded] returns true, this method will consume the events and consider
+ * that the handwriting has successfully started. Otherwise, it'll stop monitoring the current
+ * gesture.
+ */
+internal suspend inline fun PointerInputScope.detectStylusHandwriting(
+    crossinline onHandwritingSlopExceeded: () -> Boolean
+) {
+    awaitEachGesture {
+        val firstDown =
+            awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+
+        val isStylus =
+            firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
+        if (!isStylus) {
+            return@awaitEachGesture
+        }
+        // Await the touch slop before long press timeout.
+        var exceedsTouchSlop: PointerInputChange? = null
+        // The stylus move must exceeds touch slop before long press timeout.
+        while (true) {
+            val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Main)
+            // The tracked pointer is consumed or lifted, stop tracking.
+            val change = pointerEvent.changes.fastFirstOrNull {
+                !it.isConsumed && it.id == firstDown.id && it.pressed
+            }
+            if (change == null) {
+                break
+            }
+
+            val time = change.uptimeMillis - firstDown.uptimeMillis
+            if (time >= viewConfiguration.longPressTimeoutMillis) {
+                break
+            }
+
+            val offset = change.position - firstDown.position
+            if (offset.getDistance() > viewConfiguration.handwritingSlop) {
+                exceedsTouchSlop = change
+                break
+            }
+        }
+
+        if (exceedsTouchSlop == null || !onHandwritingSlopExceeded.invoke()) {
+            return@awaitEachGesture
+        }
+        exceedsTouchSlop.consume()
+
+        // Consume the remaining changes of this pointer.
+        while (true) {
+            val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
+            val pointerChange = pointerEvent.changes.fastFirstOrNull {
+                !it.isConsumed && it.id == firstDown.id && it.pressed
+            } ?: return@awaitEachGesture
+            pointerChange.consume()
+        }
+    }
+}
+
+/**
+ *  Whether the platform supports the stylus handwriting or not. This is for platform level support
+ *  and NOT for checking whether the IME supports handwriting.
+ */
+internal expect val isStylusHandwritingSupported: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
index f63d0e7b..2a587b9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
@@ -86,6 +86,16 @@
     )
 
     /**
+     * [TextFieldState] does not synchronize calls to [edit] but requires main thread access. It
+     * also has no way to disallow reentrant behavior (nested calls to [edit]) through the API.
+     * Instead we keep track of whether an edit session is currently running. If [edit] is called
+     * concurrently or reentered, it should throw an exception. The only exception is if
+     * [TextFieldState] is being modified in two different snapshots. Hence, this value is backed
+     * by a snapshot state.
+     */
+    private var isEditing: Boolean by mutableStateOf(false)
+
+    /**
      * The current text and selection. This value will automatically update when the user enters
      * text or otherwise changes the text field contents. To change it programmatically, call
      * [edit].
@@ -113,6 +123,10 @@
      * text and cursor/selection. See the documentation on [TextFieldBuffer] for a more detailed
      * description of the available operations.
      *
+     * Make sure that you do not make concurrent calls to this function or call it again inside
+     * [block]'s scope. Doing either of these actions will result in triggering an
+     * [IllegalStateException].
+     *
      * @sample androidx.compose.foundation.samples.BasicTextFieldStateEditSample
      *
      * @see setTextAndPlaceCursorAtEnd
@@ -120,8 +134,12 @@
      */
     inline fun edit(block: TextFieldBuffer.() -> Unit) {
         val mutableValue = startEdit(text)
-        mutableValue.block()
-        commitEdit(mutableValue)
+        try {
+            mutableValue.block()
+            commitEdit(mutableValue)
+        } finally {
+            finishEditing()
+        }
     }
 
     override fun toString(): String =
@@ -141,8 +159,13 @@
 
     @Suppress("ShowingMemberInHiddenClass")
     @PublishedApi
-    internal fun startEdit(value: TextFieldCharSequence): TextFieldBuffer =
-        TextFieldBuffer(value)
+    internal fun startEdit(value: TextFieldCharSequence): TextFieldBuffer {
+        check(!isEditing) {
+            "TextFieldState does not support concurrent or nested editing."
+        }
+        isEditing = true
+        return TextFieldBuffer(value)
+    }
 
     /**
      * If the text or selection in [newValue] was actually modified, updates this state's internal
@@ -163,6 +186,12 @@
         textUndoManager.clearHistory()
     }
 
+    @Suppress("ShowingMemberInHiddenClass")
+    @PublishedApi
+    internal fun finishEditing() {
+        isEditing = false
+    }
+
     /**
      * An edit block that updates [TextFieldState] on behalf of user actions such as gestures,
      * IME commands, hardware keyboard events, clipboard actions, and more. These modifications
@@ -500,7 +529,6 @@
  * If you need to store a [TextFieldState] in another object, use the [TextFieldState.Saver] object
  * to manually save and restore the state.
  */
-@ExperimentalFoundationApi
 @Composable
 fun rememberTextFieldState(
     initialText: String = "",
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.kt
index d676e39..c4617590 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.kt
@@ -63,6 +63,8 @@
         textInputModifierNode?.softwareKeyboardController?.hide()
     }
 
+    abstract fun startStylusHandwriting()
+
     interface LegacyPlatformTextInputNode {
         val softwareKeyboardController: SoftwareKeyboardController?
         val layoutCoordinates: LayoutCoordinates?
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 098f3ec..4a4d8b2 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
@@ -23,8 +23,6 @@
 import androidx.compose.foundation.content.internal.dragAndDropRequestPermission
 import androidx.compose.foundation.content.internal.getReceiveContentConfiguration
 import androidx.compose.foundation.content.readPlainText
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.text.BasicTextField
@@ -32,6 +30,8 @@
 import androidx.compose.foundation.text.KeyboardActionScope
 import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.foundation.text.input.InputTransformation
 import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
 import androidx.compose.foundation.text.input.internal.selection.TextToolbarState
@@ -45,9 +45,6 @@
 import androidx.compose.ui.input.key.KeyInputModifierNode
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.PointerType
 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.modifier.ModifierLocalModifierNode
@@ -65,7 +62,6 @@
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.platform.PlatformTextInputModifierNode
 import androidx.compose.ui.platform.PlatformTextInputSession
@@ -94,7 +90,6 @@
 import androidx.compose.ui.text.input.KeyboardCapitalization
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastFirstOrNull
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
@@ -189,15 +184,17 @@
     LayoutAwareModifierNode {
 
     private val editable get() = enabled && !readOnly
-    private var _stylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
-    private val stylusHandwritingTrigger: MutableSharedFlow<Unit>
+
+    private var backingStylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
+    private val stylusHandwritingTrigger: MutableSharedFlow<Unit>?
         get() {
-            val handwritingTrigger = _stylusHandwritingTrigger
-            if (handwritingTrigger != null) return handwritingTrigger
+            val finalStylusHandwritingTrigger = backingStylusHandwritingTrigger
+            if (finalStylusHandwritingTrigger != null) return finalStylusHandwritingTrigger
+            if (!isStylusHandwritingSupported) return null
             return MutableSharedFlow<Unit>(
                 replay = 1,
                 onBufferOverflow = BufferOverflow.DROP_LATEST
-            ).also { _stylusHandwritingTrigger = it }
+            ).also { backingStylusHandwritingTrigger = it }
         }
 
     private val pointerInputNode = delegate(SuspendingPointerInputModifierNode {
@@ -228,80 +225,35 @@
                     detectTextFieldLongPressAndAfterDrag(requestFocus)
                 }
             }
-            launch(start = CoroutineStart.UNDISPATCHED) {
-                detectStylusHandwriting()
+            // Note: when editable changes (enabled or readOnly changes), this pointerInputModifier
+            // is reset. And we don't need to worry about cancel or launch the stylus handwriting
+            // detecting job.
+            if (isStylusHandwritingSupported && editable) {
+                 launch(start = CoroutineStart.UNDISPATCHED) {
+                    detectStylusHandwriting {
+                        if (!isFocused) {
+                            requestFocus()
+                        }
+
+                        // Send the handwriting start signal to platform.
+                        // The editor should send the signal when it is focused or is about
+                        // to gain focus, Here are more details:
+                        //   1) if the editor already has an active input session, the
+                        //   platform handwriting service should already listen to this flow
+                        //   and it'll start handwriting right away.
+                        //
+                        //   2) if the editor is not focused, but it'll be focused and
+                        //   create a new input session, one handwriting signal will be
+                        //   replayed when the platform collect this flow. And the platform
+                        //   should trigger handwriting accordingly.
+                        stylusHandwritingTrigger?.tryEmit(Unit)
+                        return@detectStylusHandwriting true
+                    }
+                }
             }
         }
     })
 
-    private suspend fun PointerInputScope.detectStylusHandwriting() {
-        awaitEachGesture {
-            val firstDown =
-                awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
-
-            val isStylus =
-                firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
-            if (!editable || !isStylus) {
-                return@awaitEachGesture
-            }
-
-            val viewConfiguration = currentValueOf(LocalViewConfiguration)
-
-            // Await the touch slop before long press timeout.
-            var exceedsTouchSlop: PointerInputChange? = null
-            // The stylus move must exceeds touch slop before long press timeout.
-            while (true) {
-                val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Main)
-                // The tracked pointer is consumed or lifted, stop tracking.
-                val change = pointerEvent.changes.fastFirstOrNull {
-                    !it.isConsumed && it.id == firstDown.id && it.pressed
-                }
-                if (change == null) {
-                    break
-                }
-
-                val time = change.uptimeMillis - firstDown.uptimeMillis
-                if (time >= viewConfiguration.longPressTimeoutMillis) {
-                    break
-                }
-
-                val offset = change.position - firstDown.position
-                if (offset.getDistance() > viewConfiguration.handwritingSlop) {
-                    exceedsTouchSlop = change
-                    break
-                }
-            }
-
-            if (exceedsTouchSlop == null) return@awaitEachGesture
-
-            exceedsTouchSlop.consume()
-
-            if (!isFocused) {
-                requestFocus()
-            }
-
-            // Send the handwriting start signal to platform.
-            // The editor should send the signal when it is focused or is about to gain focused,
-            // Here are more details:
-            //   1) if the editor already has an active input session, the platform handwriting
-            //   service should already listen to this flow and it'll start handwriting right away.
-            //
-            //   2) if the editor is not focused, but it'll be focused and create a new input
-            //   session, one handwriting signal will be replayed when the platform collect this
-            //   flow. And the platform should trigger handwriting accordingly.
-            stylusHandwritingTrigger.tryEmit(Unit)
-
-            // Consume the remaining changes of this pointer.
-            while (true) {
-                val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
-                val pointerChange = pointerEvent.changes.fastFirstOrNull {
-                    !it.isConsumed && it.id == firstDown.id && it.pressed
-                } ?: return@awaitEachGesture
-                pointerChange.consume()
-            }
-        }
-    }
-
     /**
      * The last enter event that was submitted to [interactionSource] from [dragAndDropNode]. We
      * need to keep a reference to this event to send a follow-up exit event.
@@ -684,9 +636,11 @@
         // because the resize happens after the text state change, and the resize moves the cursor
         // under the keyboard. This also covers the case where the field shrinks while focused.
         val selection = textFieldState.visualText.selection
+        val layoutResult = textLayoutState.layoutResult ?: return
         if (selection.collapsed) {
             coroutineScope.launch {
-                textLayoutState.bringCursorIntoView(cursorIndex = selection.start)
+                val rect = layoutResult.getCursorRect(selection.start)
+                textLayoutState.bringIntoViewRequester.bringIntoView(rect)
             }
         }
     }
@@ -763,7 +717,7 @@
     private fun disposeInputSession() {
         inputSessionJob?.cancel()
         inputSessionJob = null
-        stylusHandwritingTrigger.resetReplayCache()
+        stylusHandwritingTrigger?.resetReplayCache()
     }
 
     private fun startInputSessionOnWindowFocusChange() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextLayoutState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextLayoutState.kt
index 2ff9d9b..feace9f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextLayoutState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextLayoutState.kt
@@ -250,14 +250,3 @@
         }
     } ?: offset
 }
-
-/**
- * Asks [TextLayoutState.bringIntoViewRequester] to bring the bounds of the cursor at [cursorIndex]
- * into view.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal suspend fun TextLayoutState.bringCursorIntoView(cursorIndex: Int) {
-    val layoutResult = layoutResult ?: return
-    val rect = layoutResult.getCursorRect(cursorIndex)
-    bringIntoViewRequester.bringIntoView(rect)
-}
diff --git a/room/room-testing/src/commonMain/kotlin/androidx/room/testing/Placeholder.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt
similarity index 82%
rename from room/room-testing/src/commonMain/kotlin/androidx/room/testing/Placeholder.kt
rename to compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt
index 3e91597..5667100 100644
--- a/room/room-testing/src/commonMain/kotlin/androidx/room/testing/Placeholder.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.room.testing
-// empty file to trigger klib creation
-// see: https://youtrack.jetbrains.com/issue/KT-52344
+package androidx.compose.foundation.text.handwriting
+
+internal actual val isStylusHandwritingSupported: Boolean = false
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt
index 5f7b203..079e229 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt
@@ -95,6 +95,10 @@
             input.focusedRect = rect
         }
     }
+
+    override fun startStylusHandwriting() {
+        // Noop for desktop
+    }
 }
 
 internal class LegacyTextInputMethodRequest(
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/PreferenceAsState.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/PreferenceAsState.kt
index 12b0a9d..417e47f 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/PreferenceAsState.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/PreferenceAsState.kt
@@ -25,9 +25,9 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.preference.PreferenceManager
 
 /**
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/SoftInputModeSetting.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/SoftInputModeSetting.kt
index da20cd1..ca9dffc 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/SoftInputModeSetting.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/SoftInputModeSetting.kt
@@ -27,8 +27,8 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.withFrameMillis
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle.State.RESUMED
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.preference.DropDownPreference
 import androidx.preference.Preference.SummaryProvider
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index e06e787..ee16cb3 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -69,7 +69,6 @@
         androidMain {
             dependsOn(jvmMain)
             dependencies {
-                implementation(project(":compose:ui:ui-graphics"))
             }
         }
 
diff --git a/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleModifierNodeTest.kt b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleModifierNodeTest.kt
index a108a27..cb70f48 100644
--- a/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleModifierNodeTest.kt
+++ b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleModifierNodeTest.kt
@@ -50,6 +50,8 @@
 import androidx.compose.ui.graphics.asAndroidBitmap
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.captureToImage
@@ -62,6 +64,8 @@
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
 import org.junit.Rule
 import org.junit.Test
@@ -153,6 +157,120 @@
         )
     }
 
+    /**
+     * Regression test for b/329693006
+     */
+    @Test
+    fun pressed_rippleCreatedBeforeDraw() {
+        // Add a static press interaction so that when the ripple is added, it will add a ripple
+        // immediately before the node is drawn
+        val interactionSource = object : MutableInteractionSource {
+            override val interactions: Flow<Interaction> =
+                flowOf(PressInteraction.Press(Offset.Zero))
+            override suspend fun emit(interaction: Interaction) {}
+            override fun tryEmit(interaction: Interaction): Boolean { return true }
+        }
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                RippleBoxWithBackground(
+                    interactionSource,
+                    TestRipple,
+                    bounded = true
+                )
+            }
+        }
+
+        val expectedColor = calculateResultingRippleColor(
+            TestRippleColor,
+            rippleOpacity = TestRippleAlpha.pressedAlpha
+        )
+
+        assertRippleMatches(
+            scope!!,
+            interactionSource,
+            // Unused
+            PressInteraction.Press(Offset(10f, 10f)),
+            expectedColor
+        )
+    }
+
+    /**
+     * Regression test for b/329693006, similar to [pressed_rippleCreatedBeforeDraw], but delegating
+     * to the ripple node later in time to simulate clickable behavior.
+     */
+    @Test
+    fun pressed_rippleLazilyDelegatedTo() {
+        // Add a static press interaction so that when the ripple is added, it will add a ripple
+        // immediately before the node is drawn
+        val interactionSource = object : MutableInteractionSource {
+            override val interactions: Flow<Interaction> =
+                flowOf(PressInteraction.Press(Offset.Zero))
+            override suspend fun emit(interaction: Interaction) {}
+            override fun tryEmit(interaction: Interaction): Boolean { return true }
+        }
+
+        class TestRippleNode : DelegatingNode() {
+            fun attachRipple() {
+                delegate(TestRipple.create(interactionSource))
+            }
+        }
+
+        val node = TestRippleNode()
+
+        val element = object : ModifierNodeElement<TestRippleNode>() {
+            override fun create(): TestRippleNode = node
+            override fun update(node: TestRippleNode) {}
+            override fun equals(other: Any?): Boolean = other === this
+            override fun hashCode(): Int = -1
+        }
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+                    Box(
+                        Modifier.padding(25.dp).background(RippleBoxBackgroundColor)
+                    ) {
+                        val shape = RoundedCornerShape(20)
+                        val clip = Modifier.clip(shape)
+                        Box(
+                            Modifier.padding(25.dp).width(40.dp).height(40.dp)
+                                .border(BorderStroke(2.dp, Color.Black), shape)
+                                .background(color = RippleBoxBackgroundColor, shape = shape)
+                                .then(clip)
+                                .then(element)
+                        ) {}
+                    }
+                }
+            }
+        }
+
+        val expectedColor = calculateResultingRippleColor(
+            TestRippleColor,
+            rippleOpacity = TestRippleAlpha.pressedAlpha
+        )
+
+        // Add the ripple node to the hierarchy, which should then create a ripple before the node
+        // has been drawn
+        rule.runOnIdle {
+            node.attachRipple()
+        }
+
+        assertRippleMatches(
+            scope!!,
+            interactionSource,
+            // Unused
+            PressInteraction.Press(Offset(10f, 10f)),
+            expectedColor
+        )
+    }
+
     @Test
     fun hovered() {
         val interactionSource = MutableInteractionSource()
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/Ripple.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/Ripple.android.kt
index 6eaccdb..766e2de5 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/Ripple.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/Ripple.android.kt
@@ -126,18 +126,7 @@
             invalidateDraw()
         }
 
-    /**
-     * Cache the size of the canvas we will draw the ripple into - this is updated each time
-     * [draw] is called. This is needed as before we start animating the ripple, we
-     * need to know its size (changing the bounds mid-animation will cause us to continue the
-     * animation on the UI thread, not the render thread), but the size is only known inside the
-     * draw scope.
-     */
-    private var rippleSize: Size = Size.Zero
-
     override fun DrawScope.drawRipples() {
-        rippleSize = size
-
         drawIntoCanvas { canvas ->
             rippleHostView?.run {
                 // We set these inside addRipple() already, but they may change during the ripple
@@ -146,9 +135,14 @@
                 // currently drawn ripples if the ripples are being drawn on the RenderThread,
                 // since only the software paint is updated, not the hardware paint used in
                 // RippleForeground.
-                updateRippleProperties(
-                    size = size,
-                    radius = targetRadius.roundToInt(),
+                // Radius updates will not take effect until the next ripple, so if the size changes
+                // the only way to update the calculated radius is by using
+                // RippleDrawable.RADIUS_AUTO to calculate the radius from the bounds automatically.
+                // But in this case, if the bounds change, the animation will switch to the UI
+                // thread instead of render thread, so this isn't clearly desired either.
+                // b/183019123
+                setRippleProperties(
+                    size = rippleSize,
                     color = rippleColor,
                     alpha = rippleAlpha().pressedAlpha
                 )
@@ -158,13 +152,13 @@
         }
     }
 
-    override fun addRipple(interaction: PressInteraction.Press) {
+    override fun addRipple(interaction: PressInteraction.Press, size: Size, targetRadius: Float) {
         rippleHostView = with(getOrCreateRippleContainer()) {
             getRippleHostView().apply {
                 addRipple(
                     interaction = interaction,
                     bounded = bounded,
-                    size = rippleSize,
+                    size = size,
                     radius = targetRadius.roundToInt(),
                     color = rippleColor,
                     alpha = rippleAlpha().pressedAlpha,
@@ -280,9 +274,8 @@
                 // currently drawn ripples if the ripples are being drawn on the RenderThread,
                 // since only the software paint is updated, not the hardware paint used in
                 // RippleForeground.
-                updateRippleProperties(
+                setRippleProperties(
                     size = size,
-                    radius = rippleRadius,
                     color = color,
                     alpha = alpha
                 )
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleContainer.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleContainer.android.kt
index 7bd6362..30c28cb 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleContainer.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleContainer.android.kt
@@ -18,7 +18,7 @@
 
 import android.content.Context
 import android.view.ViewGroup
-import androidx.compose.ui.graphics.R
+import androidx.compose.ui.R
 
 internal interface RippleHostKey {
     /**
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 fa4fa68..aa462e2 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
@@ -135,7 +135,8 @@
         }
         val ripple = ripple!!
         this.onInvalidateRipple = onInvalidateRipple
-        updateRippleProperties(size, radius, color, alpha)
+        ripple.trySetRadius(radius)
+        setRippleProperties(size, color, alpha)
         if (bounded) {
             // Bounded ripples should animate from the press position
             ripple.setHotspot(interaction.pressPosition.x, interaction.pressPosition.y)
@@ -161,13 +162,10 @@
     }
 
     /**
-     * Update the underlying [RippleDrawable] with the new properties. Note that changes to
-     * [size] or [radius] while a ripple is animating will cause the animation to move to the UI
-     * thread, so it is important to also provide the correct values in [addRipple].
+     * Update the underlying [RippleDrawable] with the new properties.
      */
-    fun updateRippleProperties(
+    fun setRippleProperties(
         size: Size,
-        radius: Int,
         color: Color,
         alpha: Float
     ) {
@@ -176,7 +174,6 @@
         // (either here or internally in RippleDrawable). Many properties invalidate the ripple when
         // changed, which will lead to a call to updateRippleProperties again, which will cause
         // another invalidation, etc.
-        ripple.trySetRadius(radius)
         ripple.setColor(color, alpha)
         val newBounds = Rect(
             0,
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/CommonRipple.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/CommonRipple.kt
index 112c2be..7dfe12a 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/CommonRipple.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/CommonRipple.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateMapOf
 import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorProducer
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -72,7 +73,7 @@
 ) : RippleNode(interactionSource, bounded, radius, color, rippleAlpha) {
     private val ripples = MutableScatterMap<PressInteraction.Press, RippleAnimation>()
 
-    override fun addRipple(interaction: PressInteraction.Press) {
+    override fun addRipple(interaction: PressInteraction.Press, size: Size, targetRadius: Float) {
         // Finish existing ripples
         ripples.forEach { _, ripple -> ripple.finish() }
         val origin = if (bounded) interaction.pressPosition else null
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index 483a851..d747be5f 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.material.ripple
 
+import androidx.collection.mutableObjectListOf
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.LinearEasing
@@ -34,6 +35,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorProducer
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -44,9 +46,13 @@
 import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.LayoutAwareModifierNode
 import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.requireDensity
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.isUnspecified
+import androidx.compose.ui.unit.toSize
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
@@ -320,40 +326,80 @@
     private val radius: Dp,
     private val color: ColorProducer,
     protected val rippleAlpha: () -> RippleAlpha
-) : Modifier.Node(), CompositionLocalConsumerModifierNode, DrawModifierNode {
+) : Modifier.Node(),
+    CompositionLocalConsumerModifierNode,
+    DrawModifierNode,
+    LayoutAwareModifierNode {
     final override val shouldAutoInvalidate: Boolean = false
 
     private var stateLayer: StateLayer? = null
 
-    // Calculated inside draw(). This won't happen in Robolectric, so default to 0f to avoid crashes
-    var targetRadius: Float = 0f
+    // The following are calculated inside onRemeasured(). These must be initialized before adding
+    // a ripple.
+
+    // Target radius updating over time for existing ripples isn't supported for Android, and
+    // isn't implemented in common, so for now it can be private.
+    private var targetRadius: Float = 0f
+    // The size is needed for Android to update ripple bounds if the size changes
+    protected var rippleSize: Size = Size.Zero
         private set
 
     val rippleColor: Color
         get() = color()
 
-    final override fun onAttach() {
+    // Track interactions that were emitted before we have been placed - we need to wait until we
+    // have a valid size in order to set the radius and size correctly.
+    private var hasValidSize = false
+    private val pendingInteractions = mutableObjectListOf<PressInteraction>()
+
+    override fun onRemeasured(size: IntSize) {
+        hasValidSize = true
+        val density = requireDensity()
+        rippleSize = size.toSize()
+        targetRadius = with(density) {
+            if (radius.isUnspecified) {
+                // Explicitly calculate the radius instead of using RippleDrawable.RADIUS_AUTO on
+                // Android since the latest spec does not match with the existing radius calculation
+                // in the framework.
+                getRippleEndRadius(bounded, rippleSize)
+            } else {
+                radius.toPx()
+            }
+        }
+        // Flush any pending interactions that were waiting for measurement
+        pendingInteractions.forEach {
+            handlePressInteraction(it)
+        }
+        pendingInteractions.clear()
+    }
+
+    override fun onAttach() {
         coroutineScope.launch {
             interactionSource.interactions.collect { interaction ->
                 when (interaction) {
-                    is PressInteraction.Press -> addRipple(interaction)
-                    is PressInteraction.Release -> removeRipple(interaction.press)
-                    is PressInteraction.Cancel -> removeRipple(interaction.press)
+                    is PressInteraction -> {
+                        if (hasValidSize) {
+                            handlePressInteraction(interaction)
+                        } else {
+                            // Handle these later when we have a valid size
+                            pendingInteractions += interaction
+                        }
+                    }
                     else -> updateStateLayer(interaction, this)
                 }
             }
         }
     }
 
-    override fun ContentDrawScope.draw() {
-        targetRadius = if (radius.isUnspecified) {
-            // Explicitly calculate the radius instead of using RippleDrawable.RADIUS_AUTO on
-            // Android since the latest spec does not match with the existing radius calculation in
-            // the framework.
-            getRippleEndRadius(bounded, size)
-        } else {
-            radius.toPx()
+    private fun handlePressInteraction(pressInteraction: PressInteraction) {
+        when (pressInteraction) {
+            is PressInteraction.Press -> addRipple(pressInteraction, rippleSize, targetRadius)
+            is PressInteraction.Release -> removeRipple(pressInteraction.press)
+            is PressInteraction.Cancel -> removeRipple(pressInteraction.press)
         }
+    }
+
+    override fun ContentDrawScope.draw() {
         drawContent()
         stateLayer?.run {
             drawStateLayer(targetRadius, rippleColor)
@@ -363,7 +409,7 @@
 
     abstract fun DrawScope.drawRipples()
 
-    abstract fun addRipple(interaction: PressInteraction.Press)
+    abstract fun addRipple(interaction: PressInteraction.Press, size: Size, targetRadius: Float)
     abstract fun removeRipple(interaction: PressInteraction.Press)
     private fun updateStateLayer(interaction: Interaction, scope: CoroutineScope) {
         val stateLayer = stateLayer ?: StateLayer(bounded, rippleAlpha).also { instance ->
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 7c294bb..e58e6b2f 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1137,9 +1137,7 @@
   }
 
   public final class OutlinedTextFieldKt {
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
     method @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
     method @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
   }
 
@@ -1622,7 +1620,7 @@
 
   public final class SwipeToDismissBoxKt {
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.SwipeToDismissBoxValue> directions);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional boolean enableDismissFromStartToEnd, optional boolean enableDismissFromEndToStart, 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 SwipeToDismissBox(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional boolean enableDismissFromStartToEnd, optional boolean enableDismissFromEndToStart, optional boolean gesturesEnabled, 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 androidx.compose.material3.SwipeToDismissBoxState rememberSwipeToDismissBoxState(optional androidx.compose.material3.SwipeToDismissBoxValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissBoxValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
   }
 
@@ -1850,12 +1848,6 @@
   @androidx.compose.runtime.Immutable public final class TextFieldDefaults {
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void DecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void FilledContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void OutlinedBorderContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void OutlinedTextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void OutlinedTextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void TextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void TextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long focusedContainerColor, optional long unfocusedContainerColor, optional long disabledContainerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors? selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
     method public androidx.compose.foundation.layout.PaddingValues contentPaddingWithLabel(optional float start, optional float end, optional float top, optional float bottom);
@@ -1870,11 +1862,7 @@
     method @Deprecated public float getUnfocusedBorderThickness();
     method public float getUnfocusedIndicatorThickness();
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public androidx.compose.ui.Modifier indicatorLine(androidx.compose.ui.Modifier, boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional float focusedIndicatorLineThickness, optional float unfocusedIndicatorLineThickness);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors outlinedTextFieldColors(optional long textColor, optional long disabledTextColor, optional long containerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedBorderColor, optional long unfocusedBorderColor, optional long disabledBorderColor, optional long errorBorderColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long placeholderColor, optional long disabledPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors outlinedTextFieldColors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long containerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedBorderColor, optional long unfocusedBorderColor, optional long disabledBorderColor, optional long errorBorderColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
     method @Deprecated public androidx.compose.foundation.layout.PaddingValues outlinedTextFieldPadding(optional float start, optional float top, optional float end, optional float bottom);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors textFieldColors(optional long textColor, optional long disabledTextColor, optional long containerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long placeholderColor, optional long disabledPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors textFieldColors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long containerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
     method @Deprecated public androidx.compose.foundation.layout.PaddingValues textFieldWithLabelPadding(optional float start, optional float end, optional float top, optional float bottom);
     method @Deprecated public androidx.compose.foundation.layout.PaddingValues textFieldWithoutLabelPadding(optional float start, optional float top, optional float end, optional float bottom);
     property @Deprecated public final float FocusedBorderThickness;
@@ -1890,9 +1878,7 @@
   }
 
   public final class TextFieldKt {
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
     method @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
     method @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 7c294bb..e58e6b2f 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1137,9 +1137,7 @@
   }
 
   public final class OutlinedTextFieldKt {
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
     method @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
     method @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
   }
 
@@ -1622,7 +1620,7 @@
 
   public final class SwipeToDismissBoxKt {
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.SwipeToDismissBoxValue> directions);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional boolean enableDismissFromStartToEnd, optional boolean enableDismissFromEndToStart, 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 SwipeToDismissBox(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional boolean enableDismissFromStartToEnd, optional boolean enableDismissFromEndToStart, optional boolean gesturesEnabled, 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 androidx.compose.material3.SwipeToDismissBoxState rememberSwipeToDismissBoxState(optional androidx.compose.material3.SwipeToDismissBoxValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissBoxValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
   }
 
@@ -1850,12 +1848,6 @@
   @androidx.compose.runtime.Immutable public final class TextFieldDefaults {
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void DecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void FilledContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void OutlinedBorderContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void OutlinedTextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void OutlinedTextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void TextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void TextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long focusedContainerColor, optional long unfocusedContainerColor, optional long disabledContainerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors? selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
     method public androidx.compose.foundation.layout.PaddingValues contentPaddingWithLabel(optional float start, optional float end, optional float top, optional float bottom);
@@ -1870,11 +1862,7 @@
     method @Deprecated public float getUnfocusedBorderThickness();
     method public float getUnfocusedIndicatorThickness();
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public androidx.compose.ui.Modifier indicatorLine(androidx.compose.ui.Modifier, boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional float focusedIndicatorLineThickness, optional float unfocusedIndicatorLineThickness);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors outlinedTextFieldColors(optional long textColor, optional long disabledTextColor, optional long containerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedBorderColor, optional long unfocusedBorderColor, optional long disabledBorderColor, optional long errorBorderColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long placeholderColor, optional long disabledPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors outlinedTextFieldColors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long containerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedBorderColor, optional long unfocusedBorderColor, optional long disabledBorderColor, optional long errorBorderColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
     method @Deprecated public androidx.compose.foundation.layout.PaddingValues outlinedTextFieldPadding(optional float start, optional float top, optional float end, optional float bottom);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors textFieldColors(optional long textColor, optional long disabledTextColor, optional long containerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long placeholderColor, optional long disabledPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors textFieldColors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long containerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
     method @Deprecated public androidx.compose.foundation.layout.PaddingValues textFieldWithLabelPadding(optional float start, optional float end, optional float top, optional float bottom);
     method @Deprecated public androidx.compose.foundation.layout.PaddingValues textFieldWithoutLabelPadding(optional float start, optional float top, optional float end, optional float bottom);
     property @Deprecated public final float FocusedBorderThickness;
@@ -1890,9 +1878,7 @@
   }
 
   public final class TextFieldKt {
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
     method @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
-    method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
     method @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
   }
 
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt
index 102eb3c..2e153e1 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt
@@ -40,7 +40,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
-import androidx.compose.testutils.expectAssertionError
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -739,14 +738,12 @@
     @OptIn(ExperimentalMaterial3Api::class)
     @Test
     fun slider_rowWithInfiniteWidth() {
-        expectAssertionError(false) {
-            rule.setContent {
-                Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
-                    Slider(
-                        state = SliderState(0f),
-                        modifier = Modifier.weight(1f)
-                    )
-                }
+        rule.setContent {
+            Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
+                Slider(
+                    state = SliderState(0f),
+                    modifier = Modifier.weight(1f)
+                )
             }
         }
     }
@@ -1393,14 +1390,12 @@
     @Test
     fun rangeSlider_rowWithInfiniteWidth() {
         val state = RangeSliderState(0f, 1f)
-        expectAssertionError(false) {
-            rule.setContent {
-                Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
-                    RangeSlider(
-                        state = state,
-                        modifier = Modifier.weight(1f)
-                    )
-                }
+        rule.setContent {
+            Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
+                RangeSlider(
+                    state = state,
+                    modifier = Modifier.weight(1f)
+                )
             }
         }
     }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
index 425848b..bc83b9c 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
@@ -395,4 +395,36 @@
             .that(newItem!!.anchoredDraggableState.anchors.size)
             .isAtLeast(1)
     }
+
+    @Test
+    fun swipeDismiss_respectsGesturesEnabled() {
+        lateinit var swipeToDismissBoxState: SwipeToDismissBoxState
+        rule.setContent {
+            swipeToDismissBoxState = rememberSwipeToDismissBoxState(SwipeToDismissBoxValue.Settled)
+            SwipeToDismissBox(
+                state = swipeToDismissBoxState,
+                modifier = Modifier.testTag(swipeDismissTag),
+                gesturesEnabled = false,
+                backgroundContent = { }
+            ) { Box(Modifier.fillMaxSize()) }
+        }
+
+        rule.onNodeWithTag(swipeDismissTag).performTouchInput { swipeRight() }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeToDismissBoxState.currentValue)
+                .isEqualTo(SwipeToDismissBoxValue.Settled)
+        }
+
+        rule.onNodeWithTag(swipeDismissTag).performTouchInput { swipeLeft() }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeToDismissBoxState.currentValue)
+                .isEqualTo(SwipeToDismissBoxValue.Settled)
+        }
+    }
 }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
index 83811d5..767e874 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
@@ -130,6 +130,58 @@
         }
     }
 
+    @Test
+    fun carousel_calculateOutOfBoundsPageCount() {
+        val xSmallSize = 5f
+        val smallSize = 100f
+        val mediumSize = 200f
+        val largeSize = 400f
+        val keylineList = keylineListOf(carouselMainAxisSize = 1000f, 0f, CarouselAlignment.Start) {
+            add(xSmallSize, isAnchor = true)
+            add(largeSize)
+            add(mediumSize)
+            add(mediumSize)
+            add(smallSize)
+            add(smallSize)
+            add(xSmallSize, isAnchor = true)
+        }
+        val strategy = Strategy { _, _ ->
+            keylineList
+        }.apply(availableSpace = 1000f, itemSpacing = 0f)
+        val outOfBoundsNum = calculateOutOfBounds(strategy)
+        // With this strategy, we expect 3 loaded items
+        val loadedItems = 3
+
+        assertThat(outOfBoundsNum).isEqualTo(
+            (keylineList.filter { !it.isAnchor }.size - loadedItems) + 1
+        )
+    }
+
+    @Test
+    fun carousel_correctlyCalculatesMaxScrollOffsetWithItemSpacing() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val state = rememberCarouselState { 10 }.also {
+                carouselState = it
+            }
+            val strategy = Strategy { availableSpace, itemSpacing ->
+                keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Start) {
+                    add(10f, isAnchor = true)
+                    add(186f)
+                    add(122f)
+                    add(56f)
+                    add(10f, isAnchor = true)
+                }
+            }.apply(availableSpace = 380f, itemSpacing = 8f)
+
+            // Max offset should only add item spacing between each item
+            val expectedMaxScrollOffset = (186f * 10) + (8f * 9) - 380f
+
+            assertThat(calculateMaxScrollOffset(state, strategy)).isEqualTo(
+                expectedMaxScrollOffset
+            )
+        }
+    }
+
     @Composable
     internal fun Item(index: Int) {
         Box(
@@ -147,7 +199,9 @@
     private fun createCarousel(
         initialItem: Int = 0,
         itemCount: () -> Int = { DefaultItemCount },
-        modifier: Modifier = Modifier.width(412.dp).height(221.dp),
+        modifier: Modifier = Modifier
+            .width(412.dp)
+            .height(221.dp),
         orientation: Orientation = Orientation.Horizontal,
         content: @Composable CarouselScope.(item: Int) -> Unit = { Item(index = it) }
     ) {
@@ -159,12 +213,12 @@
             Carousel(
                 state = state,
                 orientation = orientation,
-                keylineList = { availableSpace ->
+                keylineList = { availableSpace, itemSpacing ->
                     multiBrowseKeylineList(
                         density = density,
                         carouselMainAxisSize = availableSpace,
                         preferredItemSize = with(density) { 186.dp.toPx() },
-                        itemSpacing = 0f,
+                        itemSpacing = itemSpacing,
                         itemCount = itemCount.invoke(),
                     )
                 },
@@ -178,7 +232,9 @@
     private fun createUncontainedCarousel(
         initialItem: Int = 0,
         itemCount: () -> Int = { DefaultItemCount },
-        modifier: Modifier = Modifier.width(412.dp).height(221.dp),
+        modifier: Modifier = Modifier
+            .width(412.dp)
+            .height(221.dp),
         content: @Composable CarouselScope.(item: Int) -> Unit = { Item(index = it) }
     ) {
         rule.setMaterialContent(lightColorScheme()) {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TouchExplorationStateProvider.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TouchExplorationStateProvider.android.kt
index 8a90585..5ef72e1 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TouchExplorationStateProvider.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TouchExplorationStateProvider.android.kt
@@ -28,9 +28,9 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 /**
  * It depends on the state of accessibility services to determine the current state of touch
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/ArrangementTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/ArrangementTest.kt
index 59d6971..832b6e5 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/ArrangementTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/ArrangementTest.kt
@@ -33,6 +33,7 @@
 
         val arrangement = Arrangement.findLowestCostArrangement(
             availableSpace = targetLargeSize + targetMediumSize + targetSmallSize,
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
@@ -54,6 +55,7 @@
         val targetMediumSize = (targetLargeSize + targetSmallSize) / 2f
         val arrangement = Arrangement.findLowestCostArrangement(
             availableSpace = targetLargeSize + targetMediumSize + targetSmallSize - 10f,
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
@@ -75,6 +77,7 @@
         val targetMediumSize = (targetLargeSize + targetSmallSize) / 2f
         val arrangement = Arrangement.findLowestCostArrangement(
             availableSpace = targetLargeSize + targetMediumSize + targetSmallSize + 10f,
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
@@ -99,6 +102,7 @@
             availableSpace = targetLargeSize +
                 targetMediumSize +
                 targetSmallSize - mediumAdjustment,
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
@@ -124,6 +128,7 @@
             availableSpace = targetLargeSize +
                 targetMediumSize +
                 targetSmallSize + mediumAdjustment,
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
@@ -150,6 +155,7 @@
                 targetMediumSize +
                 (targetSmallSize * 2) +
                 (smallAdjustment * 2),
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
@@ -175,6 +181,7 @@
             availableSpace = targetLargeSize +
                 targetMediumSize +
                 (targetSmallSize * 2) - (smallAdjustment * 2),
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
@@ -200,6 +207,7 @@
                 (targetMediumSize * 2) +
                 (targetSmallSize * 2) +
                 (mediumAdjustment * 2),
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
@@ -225,6 +233,7 @@
             availableSpace = (targetLargeSize * 2) +
                 (targetMediumSize * 2) +
                 (targetSmallSize * 2) - (mediumAdjustment * 2),
+            itemSpacing = 0f,
             targetSmallSize = targetSmallSize,
             minSmallSize = 40f,
             maxSmallSize = 56f,
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
index 86a5b81..9e32c8e 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
@@ -83,7 +83,11 @@
         val smallSize = 100f
         val mediumSize = 200f
         val largeSize = 400f
-        val keylineList = keylineListOf(carouselMainAxisSize = 1000f, CarouselAlignment.Center) {
+        val keylineList = keylineListOf(
+            carouselMainAxisSize = 1000f,
+            itemSpacing = 0f,
+            carouselAlignment = CarouselAlignment.Center
+        ) {
             add(xSmallSize, isAnchor = true)
             add(smallSize)
             add(mediumSize)
@@ -93,7 +97,7 @@
             add(xSmallSize, isAnchor = true)
         }
 
-        return Strategy { keylineList }.apply(1000f)
+        return Strategy { _, _ -> keylineList }.apply(availableSpace = 1000f, itemSpacing = 0f)
     }
 
     // Test strategy that is start aligned:
@@ -110,7 +114,11 @@
         val smallSize = 100f
         val mediumSize = 200f
         val largeSize = 400f
-        val keylineList = keylineListOf(carouselMainAxisSize = 1000f, CarouselAlignment.Start) {
+        val keylineList = keylineListOf(
+            carouselMainAxisSize = 1000f,
+            itemSpacing = 0f,
+            carouselAlignment = CarouselAlignment.Start
+        ) {
             add(xSmallSize, isAnchor = true)
             add(largeSize)
             add(mediumSize)
@@ -119,7 +127,7 @@
             add(smallSize)
             add(xSmallSize, isAnchor = true)
         }
-        return Strategy { keylineList }.apply(1000f)
+        return Strategy { _, _ -> keylineList }.apply(availableSpace = 1000f, itemSpacing = 0f)
     }
 
     // Test strategy that is start aligned:
@@ -134,7 +142,7 @@
         val smallSize = 75f
         val mediumSize = 125f
         val largeSize = 400f
-        val keylineList = keylineListOf(carouselMainAxisSize = 1000f, CarouselAlignment.Start) {
+        val keylineList = keylineListOf(carouselMainAxisSize = 1000f, 0f, CarouselAlignment.Start) {
             add(xSmallSize, isAnchor = true)
             add(largeSize)
             add(largeSize)
@@ -142,6 +150,6 @@
             add(smallSize)
             add(xSmallSize, isAnchor = true)
         }
-        return Strategy { keylineList }.apply(1000f)
+        return Strategy { _, _ -> keylineList }.apply(1000f, 0f)
     }
 }
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineTest.kt
index 2fdb017..f47697e 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineTest.kt
@@ -35,7 +35,11 @@
     @Test
     fun testKeylineList_pivotsWithCorrectCutoffsRight() {
         val carouselMainAxisSize = LargeSize + (MediumSize * .75f).roundToInt()
-        val keylineList = keylineListOf(carouselMainAxisSize, CarouselAlignment.Start) {
+        val keylineList = keylineListOf(
+            carouselMainAxisSize = carouselMainAxisSize,
+            itemSpacing = 0f,
+            carouselAlignment = CarouselAlignment.Start
+        ) {
             add(SmallSize, isAnchor = true)
             add(LargeSize)
             add(MediumSize)
@@ -49,7 +53,11 @@
     @Test
     fun testKeylineList_pivotsWithCorrectCutoffsLeft() {
         val carouselMainAxisSize = (MediumSize * .75f).roundToInt() + LargeSize
-        val keylineList = keylineListOf(carouselMainAxisSize, CarouselAlignment.End) {
+        val keylineList = keylineListOf(
+            carouselMainAxisSize = carouselMainAxisSize,
+            itemSpacing = 0f,
+            carouselAlignment = CarouselAlignment.End
+        ) {
             add(SmallSize, isAnchor = true)
             add(MediumSize)
             add(LargeSize)
@@ -95,7 +103,12 @@
     @Test
     fun testKeylineListLerp() {
         val carouselMainAxisSize = StrategyTest.large + StrategyTest.medium + StrategyTest.small
-        val from = keylineListOf(carouselMainAxisSize, 1, StrategyTest.large / 2) {
+        val from = keylineListOf(
+            carouselMainAxisSize = carouselMainAxisSize,
+            itemSpacing = 0f,
+            pivotIndex = 1,
+            pivotOffset = StrategyTest.large / 2
+        ) {
             add(StrategyTest.xSmall, isAnchor = true)
             add(StrategyTest.large)
             add(StrategyTest.medium)
@@ -103,9 +116,10 @@
             add(StrategyTest.xSmall, isAnchor = true)
         }
         val to = keylineListOf(
-            carouselMainAxisSize,
-            2,
-            StrategyTest.small + (StrategyTest.large / 2)
+            carouselMainAxisSize = carouselMainAxisSize,
+            itemSpacing = 0f,
+            pivotIndex = 2,
+            pivotOffset = StrategyTest.small + (StrategyTest.large / 2)
         ) {
             add(StrategyTest.xSmall, isAnchor = true)
             add(StrategyTest.small)
@@ -134,13 +148,21 @@
 
     @Test
     fun test_keylineListsShouldBeEqual() {
-        val keylineList1 = keylineListOf(120f, CarouselAlignment.Start) {
+        val keylineList1 = keylineListOf(
+            carouselMainAxisSize = 120f,
+            itemSpacing = 0f,
+            carouselAlignment = CarouselAlignment.Start
+        ) {
             add(10f, true)
             add(100f)
             add(20f)
             add(10f, true)
         }
-        val keylineList2 = keylineListOf(120f, CarouselAlignment.Start) {
+        val keylineList2 = keylineListOf(
+            carouselMainAxisSize = 120f,
+            itemSpacing = 0f,
+            carouselAlignment = CarouselAlignment.Start
+        ) {
             add(10f, true)
             add(100f)
             add(20f)
@@ -153,13 +175,21 @@
 
     @Test
     fun testDifferentSizedItem_keylineListsShouldNotBeEqual() {
-        val keylineList1 = keylineListOf(120f, CarouselAlignment.Start) {
+        val keylineList1 = keylineListOf(
+            carouselMainAxisSize = 120f,
+            itemSpacing = 0f,
+            carouselAlignment = CarouselAlignment.Start
+        ) {
             add(11f, true)
             add(100f)
             add(20f)
             add(10f, true)
         }
-        val keylineList2 = keylineListOf(120f, CarouselAlignment.Start) {
+        val keylineList2 = keylineListOf(
+            carouselMainAxisSize = 120f,
+            itemSpacing = 0f,
+            carouselAlignment = CarouselAlignment.Start
+        ) {
             add(10f, true)
             add(100f)
             add(20f)
@@ -170,6 +200,80 @@
         assertThat(keylineList1.hashCode()).isNotEqualTo(keylineList2.hashCode())
     }
 
+    @Test
+    fun testStartKeylines_shouldAddSpacingBetweenItems() {
+        val keylines = keylineListOf(
+            carouselMainAxisSize = 380f,
+            itemSpacing = 8f,
+            carouselAlignment = CarouselAlignment.Start
+        ) {
+            add(10f, isAnchor = true)
+            add(186f)
+            add(122f)
+            add(56f)
+            add(10f, isAnchor = true)
+        }
+
+        val actualOffsets = keylines.map { it.offset }.toFloatArray()
+        val expectedOffsets = floatArrayOf(-13f, 93f, 255f, 352f, 393f)
+        assertThat(actualOffsets).isEqualTo(expectedOffsets)
+
+        val actualUnadjustedOffsets = keylines.map { it.unadjustedOffset }.toFloatArray()
+        val expectedUnadjustedOffsets = floatArrayOf(-101f, 93f, 287f, 481f, 675f)
+        assertThat(actualUnadjustedOffsets).isEqualTo(expectedUnadjustedOffsets)
+    }
+
+    @Test
+    fun testCenteredKeylines_shouldAddSpacingBetweenItems() {
+        val keylines = keylineListOf(
+            carouselMainAxisSize = 768f,
+            itemSpacing = 8f,
+            carouselAlignment = CarouselAlignment.Center
+        ) {
+            add(10f, isAnchor = true)
+            add(56f)
+            add(122f)
+            add(186f)
+            add(186f)
+            add(122f)
+            add(56f)
+            add(10f, isAnchor = true)
+        }
+
+        val actualOffsets = keylines.map { it.offset }.toFloatArray()
+        val expectedOffsets = floatArrayOf(-13f, 28f, 125f, 287f, 481f, 643f, 740f, 781f)
+        assertThat(actualOffsets).isEqualTo(expectedOffsets)
+
+        val actualUnadjustedOffsets = keylines.map { it.unadjustedOffset }.toFloatArray()
+        val expectedUnadjustedOffsets = floatArrayOf(
+            -295f, -101f, 93f, 287f, 481f, 675f, 869f, 1063f
+        )
+        assertThat(actualUnadjustedOffsets).isEqualTo(expectedUnadjustedOffsets)
+    }
+
+    @Test
+    fun testEndKeylines_shouldAddSpacingBetweenItems() {
+        val keylines = keylineListOf(
+            carouselMainAxisSize = 380f,
+            itemSpacing = 8f,
+            carouselAlignment = CarouselAlignment.End
+        ) {
+            add(10f, isAnchor = true)
+            add(56f)
+            add(122f)
+            add(186f)
+            add(10f, isAnchor = true)
+        }
+
+        val actualOffsets = keylines.map { it.offset }.toFloatArray()
+        val expectedOffsets = floatArrayOf(-13f, 28f, 125f, 287f, 393f)
+        assertThat(actualOffsets).isEqualTo(expectedOffsets)
+
+        val actualUnadjustedOffsets = keylines.map { it.unadjustedOffset }.toFloatArray()
+        val expectedUnadjustedOffsets = floatArrayOf(-295f, -101f, 93f, 287f, 481f)
+        assertThat(actualUnadjustedOffsets).isEqualTo(expectedUnadjustedOffsets)
+    }
+
     companion object {
         private const val LargeSize = 100f
         private const val SmallSize = 20f
@@ -184,7 +288,11 @@
             // [xs-s-s-m-l-l-m-s-s-xs]
             val carouselMainAxisSize =
                 (XSmallSize * 2) + (SmallSize * 4) + (MediumSize * 2) + (LargeSize * 2)
-            return keylineListOf(carouselMainAxisSize, CarouselAlignment.Center) {
+            return keylineListOf(
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = 0f,
+                carouselAlignment = CarouselAlignment.Center
+            ) {
                 add(XSmallSize, isAnchor = true)
                 add(SmallSize)
                 add(SmallSize)
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
index 9cd40a4..1cd65d5 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
@@ -37,7 +37,7 @@
             itemSpacing = 0f,
             itemCount = 10,
         )!!
-        val strategy = Strategy { keylineList }.apply(500f)
+        val strategy = Strategy { _, _ -> keylineList }.apply(500f, 0f)
 
         assertThat(strategy.itemMainAxisSize).isEqualTo(itemSize)
     }
@@ -51,8 +51,11 @@
             preferredItemSize = itemSize,
             itemSpacing = 0f,
             itemCount = 10,
-            )!!
-        val strategy = Strategy { keylineList }.apply(100f)
+        )!!
+        val strategy = Strategy { _, _ -> keylineList }.apply(
+            availableSpace = 100f,
+            itemSpacing = 0f
+        )
         val minSmallItemSize: Float = with(Density) { StrategyDefaults.MinSmallSize.toPx() }
         val keylines = strategy.defaultKeylines
 
@@ -75,8 +78,11 @@
             preferredItemSize = 200f,
             itemSpacing = 0f,
             itemCount = 10,
-            )!!
-        val strategy = Strategy { keylineList }.apply(minSmallItemSize)
+        )!!
+        val strategy = Strategy { _, _ -> keylineList }.apply(
+            availableSpace = minSmallItemSize,
+            itemSpacing = 0f
+        )
         val keylines = strategy.defaultKeylines
 
         assertThat(strategy.itemMainAxisSize).isEqualTo(minSmallItemSize)
@@ -108,7 +114,10 @@
             itemSpacing = 0f,
             itemCount = 10,
             )!!
-        val strategy = Strategy { keylineList }.apply(carouselSize)
+        val strategy = Strategy { _, _ -> keylineList }.apply(
+            availableSpace = carouselSize,
+            itemSpacing = 0f
+        )
         val keylines = strategy.defaultKeylines
 
         // Assert that there's only one small item, and a medium item that has a size between
@@ -133,7 +142,7 @@
             itemSpacing = 0f,
             itemCount = 3,
         )!!
-        val strategy = Strategy { keylineList }.apply(carouselSize)
+        val strategy = Strategy { _, _ -> keylineList }.apply(carouselSize, 0f)
         val keylines = strategy.defaultKeylines
 
         // We originally expect a keyline list of [xSmall-Large-Large-Medium-Small-xSmall], but with
@@ -143,4 +152,35 @@
         assertThat(keylines[2].isFocal).isTrue()
         assertThat(keylines[3].size).isLessThan(keylines[2].size)
     }
+
+    fun testMultiBrowse_adjustsForItemSpacing() {
+        val keylineList = multiBrowseKeylineList(
+            density = Density,
+            carouselMainAxisSize = 380f,
+            preferredItemSize = 186f,
+            itemSpacing = 8f,
+            itemCount = 10
+        )!!
+        val strategy = Strategy { _, _ -> keylineList }.apply(380f, 8f)
+
+        assertThat(keylineList.firstFocal.size).isEqualTo(186f)
+        // Ensure the first visible item is large and aligned with the start of the container
+        assertThat(keylineList.firstFocal.offset).isEqualTo(186f / 2)
+        // Ensure the last  visible item is aligned with the end of the container
+        assertThat(keylineList.lastNonAnchor.offset + (keylineList.lastNonAnchor.size / 2f))
+            .isEqualTo(380f)
+
+        assertThat(strategy.itemMainAxisSize).isEqualTo(186f)
+        val lastVisible = strategy.defaultKeylines[3]
+        assertThat(lastVisible.size).isEqualTo(56f)
+        assertThat(lastVisible.offset).isEqualTo(380f - (56f / 2f))
+
+        val maxScrollOffset = ((186f * 10) + (8f * 10)) - 380f
+        val defaultActualUnadjustedOffsets = strategy.getKeylineListForScrollOffset(
+            0f,
+            maxScrollOffset
+        ).map { it.unadjustedOffset }.toFloatArray()
+        val defaultExpectedUnadjustedOffsets = floatArrayOf(-101f, 93f, 287f, 481f, 675f)
+        assertThat(defaultActualUnadjustedOffsets).isEqualTo(defaultExpectedUnadjustedOffsets)
+    }
 }
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
index b19c365..06052ff 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
@@ -34,7 +34,10 @@
         val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
         val defaultKeylineList = createStartAlignedKeylineList()
 
-        val strategy = Strategy { defaultKeylineList }.apply(carouselMainAxisSize)
+        val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+            availableSpace = carouselMainAxisSize,
+            itemSpacing = 0f
+        )
 
         assertThat(strategy.getKeylineListForScrollOffset(0f, maxScrollOffset))
             .isEqualTo(defaultKeylineList)
@@ -62,7 +65,10 @@
         val cutoff = 50f
         val defaultKeylineList = createStartAlignedCutoffKeylineList(cutoff = cutoff)
 
-        val strategy = Strategy { defaultKeylineList }.apply(carouselMainAxisSize)
+        val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+            availableSpace = carouselMainAxisSize,
+            itemSpacing = 0f
+        )
         val endKeylineList = strategy.getEndKeylines()
 
         assertThat(defaultKeylineList.lastNonAnchor.cutoff).isEqualTo(cutoff)
@@ -81,7 +87,10 @@
         val cutoff = 50f
         val defaultKeylineList = createEndAlignedCutoffKeylineList(cutoff = cutoff)
 
-        val strategy = Strategy { defaultKeylineList }.apply(carouselMainAxisSize)
+        val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+            availableSpace = carouselMainAxisSize,
+            itemSpacing = 0f
+        )
         val startKeylineList = strategy.getStartKeylines()
 
         assertThat(defaultKeylineList.firstNonAnchor.cutoff).isEqualTo(cutoff)
@@ -101,13 +110,21 @@
         val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
         val defaultKeylines = createCenterAlignedKeylineList()
 
-        val strategy = Strategy { defaultKeylines }.apply(carouselMainAxisSize)
+        val strategy = Strategy { _, _ -> defaultKeylines }.apply(
+            availableSpace = carouselMainAxisSize,
+            itemSpacing = 0f
+        )
 
         val startSteps = listOf(
             // default step - [xs | s s m l l m s s | xs]
             createCenterAlignedKeylineList(),
             // step 1 - [xs | s m l l m s s s | xs]
-            keylineListOf(carouselMainAxisSize, 3, (small * 1 + medium + (large / 2))) {
+            keylineListOf(
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = 0f,
+                pivotIndex = 3,
+                pivotOffset = (small * 1 + medium + (large / 2))
+            ) {
                 add(xSmall, isAnchor = true)
                 add(small)
                 add(medium)
@@ -120,7 +137,12 @@
                 add(xSmall, isAnchor = true)
             },
             // step 2 - [xs | m l l m s s s s | xs]
-            keylineListOf(carouselMainAxisSize, 2, medium + (large / 2)) {
+            keylineListOf(
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = 0f,
+                pivotIndex = 2,
+                pivotOffset = medium + (large / 2)
+            ) {
                 add(xSmall, isAnchor = true)
                 add(medium)
                 add(large)
@@ -133,7 +155,12 @@
                 add(xSmall, isAnchor = true)
             },
             // step 3 - [xs | l l m m s s s s | xs]
-            keylineListOf(carouselMainAxisSize, 1, large / 2) {
+            keylineListOf(
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = 0f,
+                pivotIndex = 1,
+                pivotOffset = large / 2
+            ) {
                 add(xSmall, isAnchor = true)
                 add(large)
                 add(large)
@@ -190,13 +217,21 @@
         val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
         val defaultKeylines = createCenterAlignedKeylineList()
 
-        val strategy = Strategy { defaultKeylines }.apply(carouselMainAxisSize)
+        val strategy = Strategy { _, _ -> defaultKeylines }.apply(
+            availableSpace = carouselMainAxisSize,
+            itemSpacing = 0f
+        )
 
         val endSteps = listOf(
             // default step
             createCenterAlignedKeylineList(),
             // step 1: Move a small item from after focal to beginning of focal
-            keylineListOf(carouselMainAxisSize, 5, (small * 3) + medium + (large / 2)) {
+            keylineListOf(
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = 0f,
+                pivotIndex = 5,
+                pivotOffset = (small * 3) + medium + (large / 2)
+            ) {
                 add(xSmall, isAnchor = true)
                 add(small)
                 add(small)
@@ -209,7 +244,12 @@
                 add(xSmall, isAnchor = true)
             },
             // step 2: Move another small from after focal to beginning of focal
-            keylineListOf(carouselMainAxisSize, 6, (small * 4) + medium + (large / 2)) {
+            keylineListOf(
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = 0f,
+                pivotIndex = 6,
+                pivotOffset = (small * 4) + medium + (large / 2)
+            ) {
                 add(xSmall, isAnchor = true)
                 add(small)
                 add(small)
@@ -223,7 +263,12 @@
             },
 
             // step 3: Move medium from after focal to beginning of focal
-            keylineListOf(carouselMainAxisSize, 7, (small * 4) + (medium * 2) + (large / 2)) {
+            keylineListOf(
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = 0f,
+                pivotIndex = 7,
+                pivotOffset = (small * 4) + (medium * 2) + (large / 2)
+            ) {
                 add(xSmall, isAnchor = true)
                 add(small)
                 add(small)
@@ -301,16 +346,16 @@
     @Test
     fun testStrategy_sameAvailableSpaceCreatesEqualObjects() {
         val itemSize = large
-        val itemSpacing = 0f
         val itemCount = 10
-        val strategy1 = Strategy { availableSpace ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount)
+        val itemSpacing = 0f
+        val strategy1 = Strategy { availableSpace, itemSpacingPx ->
+            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
         }
-        val strategy2 = Strategy { availableSpace ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount)
+        val strategy2 = Strategy { availableSpace, itemSpacingPx ->
+            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
         }
-        strategy1.apply(500f)
-        strategy2.apply(500f)
+        strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
+        strategy2.apply(availableSpace = 500f, itemSpacing = itemSpacing)
 
         assertThat(strategy1 == strategy2).isTrue()
         assertThat(strategy1.hashCode()).isEqualTo(strategy2.hashCode())
@@ -321,14 +366,14 @@
         val itemSize = large
         val itemSpacing = 0f
         val itemCount = 10
-        val strategy1 = Strategy { availableSpace ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount)
+        val strategy1 = Strategy { availableSpace, itemSpacingPx ->
+            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
         }
-        val strategy2 = Strategy { availableSpace ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount)
+        val strategy2 = Strategy { availableSpace, itemSpacingPx ->
+            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
         }
-        strategy1.apply(500f)
-        strategy2.apply(500f + 1f)
+        strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
+        strategy2.apply(availableSpace = 500f + 1f, itemSpacing = itemSpacing)
 
         assertThat(strategy1 == strategy2).isFalse()
         assertThat(strategy1.hashCode()).isNotEqualTo(strategy2.hashCode())
@@ -339,13 +384,13 @@
         val itemSize = large
         val itemSpacing = 0f
         val itemCount = 10
-        val strategy1 = Strategy { availableSpace ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount)
+        val strategy1 = Strategy { availableSpace, itemSpacingPx ->
+            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
         }
-        val strategy2 = Strategy { availableSpace ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount)
+        val strategy2 = Strategy { availableSpace, itemSpacingPx ->
+            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
         }
-        strategy1.apply(500f)
+        strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
 
         assertThat(strategy1 == strategy2).isFalse()
         assertThat(strategy1.hashCode()).isNotEqualTo(strategy2.hashCode())
@@ -358,12 +403,110 @@
         val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
         val defaultKeylineList = createStartAlignedKeylineList()
 
-        val strategy = Strategy { defaultKeylineList }.apply(carouselMainAxisSize)
+        val strategy = Strategy { _, _ -> defaultKeylineList }.apply(carouselMainAxisSize, 0f)
 
         assertThat(strategy.getKeylineListForScrollOffset(0f, maxScrollOffset))
             .isEqualTo(defaultKeylineList)
     }
 
+    @Test
+    fun testStartKeylineStrategy_endStepsShouldAccountForItemSpacing() {
+        val strategy = Strategy { availableSpace, itemSpacing ->
+            keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Start) {
+                add(10f, isAnchor = true)
+                add(186f)
+                add(122f)
+                add(56f)
+                add(10f, isAnchor = true)
+            }
+        }.apply(availableSpace = 380f, itemSpacing = 8f)
+
+        val middleStep = strategy.endKeylineSteps[1]
+        val actualMiddleOffsets = middleStep.map { it.offset }.toFloatArray()
+        val expectedMiddleOffsets = floatArrayOf(-13f, 28f, 157f, 319f, 393f)
+        assertThat(actualMiddleOffsets).isEqualTo(expectedMiddleOffsets)
+
+        val actualMiddleUnadjustedOffsets = middleStep.map { it.unadjustedOffset }.toFloatArray()
+        val expectedMiddleUnadjustedOffsets = floatArrayOf(-231f, -37f, 157f, 351f, 545f)
+        assertThat(actualMiddleUnadjustedOffsets).isEqualTo(expectedMiddleUnadjustedOffsets)
+
+        val endStep = strategy.endKeylineSteps[2]
+        val actualEndOffsets = endStep.map { it.offset }.toFloatArray()
+        val expectedEndOffsets = floatArrayOf(-13f, 28f, 125f, 287f, 393f)
+        assertThat(actualEndOffsets).isEqualTo(expectedEndOffsets)
+
+        val actualEndUnadjustedOffsets = endStep.map { it.unadjustedOffset }.toFloatArray()
+        val expectedEndUnadjustedOffsets = floatArrayOf(-295f, -101f, 93f, 287f, 481f)
+        assertThat(actualEndUnadjustedOffsets).isEqualTo(expectedEndUnadjustedOffsets)
+    }
+
+    @Test
+    fun testCenterKeylineStrategy_startAndEndStepsShouldAccountForItemSpacing() {
+        val strategy = Strategy { availableSpace, itemSpacing ->
+            keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Center) {
+                add(10f, isAnchor = true)
+                add(56f)
+                add(122f)
+                add(186f)
+                add(186f)
+                add(122f)
+                add(56f)
+                add(10f, isAnchor = true)
+            }
+        }.apply(availableSpace = 768f, itemSpacing = 8f)
+
+        assertThat(strategy.startKeylineSteps).hasSize(3)
+        assertThat(strategy.endKeylineSteps).hasSize(3)
+
+        val s1 = strategy.startKeylineSteps[1]
+        val s1ActualOffsets = s1.map { it.offset }.toFloatArray()
+        // Should move one small from start to end - xs, m, l, l, m, s, s, xs
+        val s1ExpectedOffsets = floatArrayOf(-13f, 61f, 223f, 417f, 579f, 676f, 740f, 781f)
+        assertThat(s1ActualOffsets).isEqualTo(s1ExpectedOffsets)
+
+        val s1ActualUnadjustedOffsets = s1.map { it.unadjustedOffset }.toFloatArray()
+        val s1ExpectedUnadjustedOffsets = floatArrayOf(
+            -165f, 29f, 223f, 417f, 611f, 805f, 999f, 1193f
+        )
+        assertThat(s1ActualUnadjustedOffsets).isEqualTo(s1ExpectedUnadjustedOffsets)
+
+        val s2 = strategy.startKeylineSteps[2]
+        val s2ActualOffsets = s2.map { it.offset }.toFloatArray()
+        // Should be - xs, l, l, m, m, s, s, xs
+        val s2ExpectedOffsets = floatArrayOf(-13f, 93f, 287f, 449f, 579f, 676f, 740f, 781f)
+        assertThat(s2ActualOffsets).isEqualTo(s2ExpectedOffsets)
+
+        val s2ActualUnadjustedOffsets = s2.map { it.unadjustedOffset }.toFloatArray()
+        val s2ExpectedUnadjustedOffsets = floatArrayOf(
+            -101f, 93f, 287f, 481f, 675f, 869f, 1063f, 1257f
+        )
+        assertThat(s2ActualUnadjustedOffsets).isEqualTo(s2ExpectedUnadjustedOffsets)
+
+        val e1 = strategy.endKeylineSteps[1]
+        val e1ActualOffsets = e1.map { it.offset }.toFloatArray()
+        // Should move one small item to start - xs, s, s, m, l, l, m, xs
+        val e1ExpectedOffsets = floatArrayOf(-13f, 28f, 92f, 189f, 351f, 545f, 707f, 781f)
+        assertThat(e1ActualOffsets).isEqualTo(e1ExpectedOffsets)
+
+        val e1ActualUnadjustedOffsets = e1.map { it.unadjustedOffset }.toFloatArray()
+        val e1ExpectedUnadjustedOffsets = floatArrayOf(
+            -425f, -231f, -37f, 157f, 351f, 545f, 739f, 933f
+        )
+        assertThat(e1ActualUnadjustedOffsets).isEqualTo(e1ExpectedUnadjustedOffsets)
+
+        val e2 = strategy.endKeylineSteps[2]
+        val e2ActualOffsets = e2.map { it.offset }.toFloatArray()
+        // Should move one medium item to end
+        val e2ExpectedOffsets = floatArrayOf(-13f, 28f, 92f, 189f, 319f, 481f, 675f, 781f)
+        assertThat(e2ActualOffsets).isEqualTo(e2ExpectedOffsets)
+
+        val e2ActualUnadjustedOffsets = e2.map { it.unadjustedOffset }.toFloatArray()
+        val e2ExpectedUnadjustedOffsets = floatArrayOf(
+            -489f, -295f, -101f, 93f, 287f, 481f, 675f, 869f
+        )
+        assertThat(e2ActualUnadjustedOffsets).isEqualTo(e2ExpectedUnadjustedOffsets)
+    }
+
     private fun assertEqualWithFloatTolerance(
         tolerance: Float,
         actual: Keyline,
@@ -391,7 +534,11 @@
         private fun createCenterAlignedKeylineList(): KeylineList {
             // [xs | s s m l l m s s | xs]
             val carouselMainAxisSize = (small * 2) + medium + (large * 2) + medium + (small * 2)
-            return keylineListOf(carouselMainAxisSize, CarouselAlignment.Center) {
+            return keylineListOf(
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = 0f,
+                carouselAlignment = CarouselAlignment.Center
+            ) {
                 add(xSmall, isAnchor = true)
                 add(small)
                 add(small)
@@ -407,7 +554,11 @@
 
         private fun createStartAlignedKeylineList(): KeylineList {
             // [xs | l m s | xs]
-            return keylineListOf(large + medium + small, CarouselAlignment.Start) {
+            return keylineListOf(
+                carouselMainAxisSize = large + medium + small,
+                itemSpacing = 0f,
+                carouselAlignment = CarouselAlignment.Start
+            ) {
                 add(xSmall, isAnchor = true)
                 add(large)
                 add(medium)
@@ -418,7 +569,11 @@
 
         private fun createStartAlignedCutoffKeylineList(cutoff: Float): KeylineList {
             // [xs | l m m | xs]
-            return keylineListOf(large + medium + medium, CarouselAlignment.Start) {
+            return keylineListOf(
+                carouselMainAxisSize = large + medium + medium,
+                itemSpacing = 0f,
+                carouselAlignment = CarouselAlignment.Start
+            ) {
                 add(xSmall, isAnchor = true)
                 add(large + cutoff)
                 add(medium)
@@ -429,7 +584,11 @@
 
         private fun createEndAlignedCutoffKeylineList(cutoff: Float): KeylineList {
             // [xs | m m l | xs]
-            return keylineListOf(large + medium + medium, CarouselAlignment.End) {
+            return keylineListOf(
+                carouselMainAxisSize = large + medium + medium,
+                itemSpacing = 0f,
+                carouselAlignment = CarouselAlignment.End
+            ) {
                 add(xSmall, isAnchor = true)
                 add(medium)
                 add(medium)
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
index f107932..303982d 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
@@ -37,7 +37,10 @@
             itemSize = itemSize,
             itemSpacing = 0f
         )
-        val strategy = Strategy { keylineList }.apply(carouselSize)
+        val strategy = Strategy { _, _ -> keylineList }.apply(
+            availableSpace = carouselSize,
+            itemSpacing = 0f
+        )
         val keylines = strategy.defaultKeylines
         val anchorSize = with(Density) { StrategyDefaults.AnchorSize.toPx() }
 
@@ -60,7 +63,10 @@
             itemSize = itemSize,
             itemSpacing = 0f
         )
-        val strategy = Strategy { keylineList }.apply(carouselSize)
+        val strategy = Strategy { _, _ -> keylineList }.apply(
+            availableSpace = carouselSize,
+            itemSpacing = 0f
+        )
         val keylines = strategy.defaultKeylines
         val anchorSize = with(Density) { StrategyDefaults.AnchorSize.toPx() }
 
@@ -86,7 +92,10 @@
             itemSize = itemSize,
             itemSpacing = 0f
         )
-        val strategy = Strategy { keylineList }.apply(carouselSize)
+        val strategy = Strategy { _, _ -> keylineList }.apply(
+            availableSpace = carouselSize,
+            itemSpacing = 0f
+        )
         val keylines = strategy.defaultKeylines
         val rightAnchorSize = with(Density) { StrategyDefaults.AnchorSize.toPx() }
 
@@ -121,7 +130,10 @@
             itemSize = itemSize,
             itemSpacing = 0f
         )
-        val strategy = Strategy { keylineList }.apply(carouselSize)
+        val strategy = Strategy { _, _ -> keylineList }.apply(
+            availableSpace = carouselSize,
+            itemSpacing = 0f
+        )
         val keylines = strategy.defaultKeylines
         val rightAnchorSize = with(Density) { StrategyDefaults.AnchorSize.toPx() }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
index 854a7a9..a4c1394 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
@@ -448,6 +448,8 @@
 
     internal var defaultNavigationRailItemColorsCached: NavigationRailItemColors? = null
 
+    internal var defaultExpressiveNavigationBarItemColorsCached: NavigationItemColors? = null
+
     internal var defaultRadioButtonColorsCached: RadioButtonColors? = null
 
     @OptIn(ExperimentalMaterial3Api::class)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExpressiveNavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExpressiveNavigationBar.kt
index 8887c16..c7b0179 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExpressiveNavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExpressiveNavigationBar.kt
@@ -119,19 +119,17 @@
  *
  * TODO: Remove internal.
  */
-@ExperimentalMaterial3Api
 internal object ExpressiveNavigationBarItemDefaults {
     /**
      * Creates a [NavigationItemColors] with the provided colors according to the Material
      * specification.
      */
     @Composable
-    fun colors() = MaterialTheme.colorScheme.expressiveNavigationBarItemColors
+    fun colors() = MaterialTheme.colorScheme.defaultExpressiveNavigationBarItemColors
 
-    // TODO: Add a cached expressiveNavigationBarItemColors.
-    internal val ColorScheme.expressiveNavigationBarItemColors: NavigationItemColors
+    internal val ColorScheme.defaultExpressiveNavigationBarItemColors: NavigationItemColors
         get() {
-            return NavigationItemColors(
+            return defaultExpressiveNavigationBarItemColorsCached ?: NavigationItemColors(
                 selectedIconColor = fromToken(ActiveIconColor),
                 selectedTextColor = fromToken(ActiveLabelTextColor),
                 selectedIndicatorColor = fromToken(ActiveIndicatorColor),
@@ -139,7 +137,9 @@
                 unselectedTextColor = fromToken(InactiveLabelTextColor),
                 disabledIconColor = fromToken(InactiveIconColor).copy(alpha = DisabledAlpha),
                 disabledTextColor = fromToken(InactiveLabelTextColor).copy(alpha = DisabledAlpha),
-            )
+            ).also {
+                defaultExpressiveNavigationBarItemColorsCached = it
+            }
         }
 }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
index 15e6155..1c26998 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
@@ -105,7 +105,6 @@
  * TODO: Remove "internal".
  */
 @Immutable
-@ExperimentalMaterial3Api
 internal class NavigationItemColors constructor(
     val selectedIconColor: Color,
     val selectedTextColor: Color,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index fe9240c..6c210fe 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -400,112 +400,6 @@
     }
 }
 
-@Deprecated("Use overload with prefix and suffix parameters", level = DeprecationLevel.HIDDEN)
-@ExperimentalMaterial3Api
-@Composable
-fun OutlinedTextField(
-    value: String,
-    onValueChange: (String) -> Unit,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    readOnly: Boolean = false,
-    textStyle: TextStyle = LocalTextStyle.current,
-    label: @Composable (() -> Unit)? = null,
-    placeholder: @Composable (() -> Unit)? = null,
-    leadingIcon: @Composable (() -> Unit)? = null,
-    trailingIcon: @Composable (() -> Unit)? = null,
-    supportingText: @Composable (() -> Unit)? = null,
-    isError: Boolean = false,
-    visualTransformation: VisualTransformation = VisualTransformation.None,
-    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
-    keyboardActions: KeyboardActions = KeyboardActions.Default,
-    singleLine: Boolean = false,
-    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
-    minLines: Int = 1,
-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = OutlinedTextFieldDefaults.shape,
-    colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
-) {
-    OutlinedTextField(
-        value = value,
-        onValueChange = onValueChange,
-        modifier = modifier,
-        enabled = enabled,
-        readOnly = readOnly,
-        textStyle = textStyle,
-        label = label,
-        placeholder = placeholder,
-        leadingIcon = leadingIcon,
-        trailingIcon = trailingIcon,
-        prefix = null,
-        suffix = null,
-        supportingText = supportingText,
-        isError = isError,
-        visualTransformation = visualTransformation,
-        keyboardOptions = keyboardOptions,
-        keyboardActions = keyboardActions,
-        singleLine = singleLine,
-        maxLines = maxLines,
-        minLines = minLines,
-        interactionSource = interactionSource,
-        shape = shape,
-        colors = colors,
-    )
-}
-
-@Deprecated("Use overload with prefix and suffix parameters", level = DeprecationLevel.HIDDEN)
-@ExperimentalMaterial3Api
-@Composable
-fun OutlinedTextField(
-    value: TextFieldValue,
-    onValueChange: (TextFieldValue) -> Unit,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    readOnly: Boolean = false,
-    textStyle: TextStyle = LocalTextStyle.current,
-    label: @Composable (() -> Unit)? = null,
-    placeholder: @Composable (() -> Unit)? = null,
-    leadingIcon: @Composable (() -> Unit)? = null,
-    trailingIcon: @Composable (() -> Unit)? = null,
-    supportingText: @Composable (() -> Unit)? = null,
-    isError: Boolean = false,
-    visualTransformation: VisualTransformation = VisualTransformation.None,
-    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
-    keyboardActions: KeyboardActions = KeyboardActions.Default,
-    singleLine: Boolean = false,
-    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
-    minLines: Int = 1,
-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = OutlinedTextFieldDefaults.shape,
-    colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
-) {
-    OutlinedTextField(
-        value = value,
-        onValueChange = onValueChange,
-        modifier = modifier,
-        enabled = enabled,
-        readOnly = readOnly,
-        textStyle = textStyle,
-        label = label,
-        placeholder = placeholder,
-        leadingIcon = leadingIcon,
-        trailingIcon = trailingIcon,
-        prefix = null,
-        suffix = null,
-        supportingText = supportingText,
-        isError = isError,
-        visualTransformation = visualTransformation,
-        keyboardOptions = keyboardOptions,
-        keyboardActions = keyboardActions,
-        singleLine = singleLine,
-        maxLines = maxLines,
-        minLines = minLines,
-        interactionSource = interactionSource,
-        shape = shape,
-        colors = colors,
-    )
-}
-
 /**
  * Layout of the leading and trailing icons and the text field, label and placeholder in
  * [OutlinedTextField].
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
index 7a6d278..c55ac5116 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
@@ -272,6 +272,7 @@
  * @param modifier Optional [Modifier] for this component.
  * @param enableDismissFromStartToEnd Whether SwipeToDismissBox can be dismissed from start to end.
  * @param enableDismissFromEndToStart Whether SwipeToDismissBox can be dismissed from end to start.
+ * @param gesturesEnabled Whether swipe-to-dismiss can be interacted by gestures.
  * @param content The content that can be dismissed.
  */
 @Composable
@@ -282,6 +283,7 @@
     modifier: Modifier = Modifier,
     enableDismissFromStartToEnd: Boolean = true,
     enableDismissFromEndToStart: Boolean = true,
+    gesturesEnabled: Boolean = true,
     content: @Composable RowScope.() -> Unit,
 ) {
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
@@ -291,7 +293,7 @@
             .anchoredDraggable(
                 state = state.anchoredDraggableState,
                 orientation = Orientation.Horizontal,
-                enabled = state.currentValue == SwipeToDismissBoxValue.Settled,
+                enabled = gesturesEnabled && state.currentValue == SwipeToDismissBoxValue.Settled,
             ),
         propagateMinConstraints = true
     ) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index 9cbcf10..6f8d337 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -398,112 +398,6 @@
     }
 }
 
-@Deprecated("Use overload with prefix and suffix parameters", level = DeprecationLevel.HIDDEN)
-@ExperimentalMaterial3Api
-@Composable
-fun TextField(
-    value: String,
-    onValueChange: (String) -> Unit,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    readOnly: Boolean = false,
-    textStyle: TextStyle = LocalTextStyle.current,
-    label: @Composable (() -> Unit)? = null,
-    placeholder: @Composable (() -> Unit)? = null,
-    leadingIcon: @Composable (() -> Unit)? = null,
-    trailingIcon: @Composable (() -> Unit)? = null,
-    supportingText: @Composable (() -> Unit)? = null,
-    isError: Boolean = false,
-    visualTransformation: VisualTransformation = VisualTransformation.None,
-    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
-    keyboardActions: KeyboardActions = KeyboardActions.Default,
-    singleLine: Boolean = false,
-    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
-    minLines: Int = 1,
-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = TextFieldDefaults.shape,
-    colors: TextFieldColors = TextFieldDefaults.colors()
-) {
-    TextField(
-        value = value,
-        onValueChange = onValueChange,
-        modifier = modifier,
-        enabled = enabled,
-        readOnly = readOnly,
-        textStyle = textStyle,
-        label = label,
-        placeholder = placeholder,
-        leadingIcon = leadingIcon,
-        trailingIcon = trailingIcon,
-        prefix = null,
-        suffix = null,
-        supportingText = supportingText,
-        isError = isError,
-        visualTransformation = visualTransformation,
-        keyboardOptions = keyboardOptions,
-        keyboardActions = keyboardActions,
-        singleLine = singleLine,
-        maxLines = maxLines,
-        minLines = minLines,
-        interactionSource = interactionSource,
-        shape = shape,
-        colors = colors,
-    )
-}
-
-@Deprecated("Use overload with prefix and suffix parameters", level = DeprecationLevel.HIDDEN)
-@ExperimentalMaterial3Api
-@Composable
-fun TextField(
-    value: TextFieldValue,
-    onValueChange: (TextFieldValue) -> Unit,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    readOnly: Boolean = false,
-    textStyle: TextStyle = LocalTextStyle.current,
-    label: @Composable (() -> Unit)? = null,
-    placeholder: @Composable (() -> Unit)? = null,
-    leadingIcon: @Composable (() -> Unit)? = null,
-    trailingIcon: @Composable (() -> Unit)? = null,
-    supportingText: @Composable (() -> Unit)? = null,
-    isError: Boolean = false,
-    visualTransformation: VisualTransformation = VisualTransformation.None,
-    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
-    keyboardActions: KeyboardActions = KeyboardActions.Default,
-    singleLine: Boolean = false,
-    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
-    minLines: Int = 1,
-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = TextFieldDefaults.shape,
-    colors: TextFieldColors = TextFieldDefaults.colors()
-) {
-    TextField(
-        value = value,
-        onValueChange = onValueChange,
-        modifier = modifier,
-        enabled = enabled,
-        readOnly = readOnly,
-        textStyle = textStyle,
-        label = label,
-        placeholder = placeholder,
-        leadingIcon = leadingIcon,
-        trailingIcon = trailingIcon,
-        prefix = null,
-        suffix = null,
-        supportingText = supportingText,
-        isError = isError,
-        visualTransformation = visualTransformation,
-        keyboardOptions = keyboardOptions,
-        keyboardActions = keyboardActions,
-        singleLine = singleLine,
-        maxLines = maxLines,
-        minLines = minLines,
-        interactionSource = interactionSource,
-        shape = shape,
-        colors = colors,
-    )
-}
-
 /**
  * Composable responsible for measuring and laying out leading and trailing icons, label,
  * placeholder and the input field.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
index 735c5b6..ba49cea 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
@@ -557,67 +557,6 @@
     val FocusedBorderThickness = FocusedIndicatorThickness
 
     @Deprecated(
-        message = "Renamed to `TextFieldDefaults.ContainerBox`",
-        replaceWith = ReplaceWith("TextFieldDefaults.ContainerBox(\n" +
-            "        enabled = enabled,\n" +
-            "        isError = isError,\n" +
-            "        interactionSource = interactionSource,\n" +
-            "        colors = colors,\n" +
-            "        shape = shape,\n" +
-            "    )"),
-        level = DeprecationLevel.WARNING
-    )
-    @ExperimentalMaterial3Api
-    @Composable
-    fun FilledContainerBox(
-        enabled: Boolean,
-        isError: Boolean,
-        interactionSource: InteractionSource,
-        colors: TextFieldColors,
-        shape: Shape = TextFieldDefaults.shape,
-    ) = ContainerBox(
-        enabled = enabled,
-        isError = isError,
-        interactionSource = interactionSource,
-        colors = colors,
-        shape = shape,
-    )
-
-    @Deprecated(
-        message = "Renamed to `OutlinedTextFieldDefaults.ContainerBox`",
-        replaceWith = ReplaceWith("OutlinedTextFieldDefaults.ContainerBox(\n" +
-            "        enabled = enabled,\n" +
-            "        isError = isError,\n" +
-            "        interactionSource = interactionSource,\n" +
-            "        colors = colors,\n" +
-            "        shape = shape,\n" +
-            "        focusedBorderThickness = focusedBorderThickness,\n" +
-            "        unfocusedBorderThickness = unfocusedBorderThickness,\n" +
-            "    )",
-            "androidx.compose.material.OutlinedTextFieldDefaults"),
-        level = DeprecationLevel.WARNING
-    )
-    @ExperimentalMaterial3Api
-    @Composable
-    fun OutlinedBorderContainerBox(
-        enabled: Boolean,
-        isError: Boolean,
-        interactionSource: InteractionSource,
-        colors: TextFieldColors,
-        shape: Shape = OutlinedTextFieldTokens.ContainerShape.value,
-        focusedBorderThickness: Dp = OutlinedTextFieldDefaults.FocusedBorderThickness,
-        unfocusedBorderThickness: Dp = OutlinedTextFieldDefaults.UnfocusedBorderThickness
-    ) = OutlinedTextFieldDefaults.ContainerBox(
-        enabled = enabled,
-        isError = isError,
-        interactionSource = interactionSource,
-        colors = colors,
-        shape = shape,
-        focusedBorderThickness = focusedBorderThickness,
-        unfocusedBorderThickness = unfocusedBorderThickness,
-    )
-
-    @Deprecated(
         message = "Renamed to `TextFieldDefaults.contentPaddingWithLabel`",
         replaceWith = ReplaceWith("TextFieldDefaults.contentPaddingWithLabel(\n" +
             "        start = start,\n" +
@@ -683,727 +622,6 @@
         end = end,
         bottom = bottom,
     )
-
-    @Deprecated(
-        message = "Renamed to `TextFieldDefaults.colors` with additional parameters to control" +
-            "container color based on state.",
-        replaceWith = ReplaceWith("TextFieldDefaults.colors(\n" +
-            "        focusedTextColor = focusedTextColor,\n" +
-            "        unfocusedTextColor = unfocusedTextColor,\n" +
-            "        disabledTextColor = disabledTextColor,\n" +
-            "        errorTextColor = errorTextColor,\n" +
-            "        focusedContainerColor = containerColor,\n" +
-            "        unfocusedContainerColor = containerColor,\n" +
-            "        disabledContainerColor = containerColor,\n" +
-            "        errorContainerColor = errorContainerColor,\n" +
-            "        cursorColor = cursorColor,\n" +
-            "        errorCursorColor = errorCursorColor,\n" +
-            "        selectionColors = selectionColors,\n" +
-            "        focusedIndicatorColor = focusedIndicatorColor,\n" +
-            "        unfocusedIndicatorColor = unfocusedIndicatorColor,\n" +
-            "        disabledIndicatorColor = disabledIndicatorColor,\n" +
-            "        errorIndicatorColor = errorIndicatorColor,\n" +
-            "        focusedLeadingIconColor = focusedLeadingIconColor,\n" +
-            "        unfocusedLeadingIconColor = unfocusedLeadingIconColor,\n" +
-            "        disabledLeadingIconColor = disabledLeadingIconColor,\n" +
-            "        errorLeadingIconColor = errorLeadingIconColor,\n" +
-            "        focusedTrailingIconColor = focusedTrailingIconColor,\n" +
-            "        unfocusedTrailingIconColor = unfocusedTrailingIconColor,\n" +
-            "        disabledTrailingIconColor = disabledTrailingIconColor,\n" +
-            "        errorTrailingIconColor = errorTrailingIconColor,\n" +
-            "        focusedLabelColor = focusedLabelColor,\n" +
-            "        unfocusedLabelColor = unfocusedLabelColor,\n" +
-            "        disabledLabelColor = disabledLabelColor,\n" +
-            "        errorLabelColor = errorLabelColor,\n" +
-            "        focusedPlaceholderColor = focusedPlaceholderColor,\n" +
-            "        unfocusedPlaceholderColor = unfocusedPlaceholderColor,\n" +
-            "        disabledPlaceholderColor = disabledPlaceholderColor,\n" +
-            "        errorPlaceholderColor = errorPlaceholderColor,\n" +
-            "        focusedSupportingTextColor = focusedSupportingTextColor,\n" +
-            "        unfocusedSupportingTextColor = unfocusedSupportingTextColor,\n" +
-            "        disabledSupportingTextColor = disabledSupportingTextColor,\n" +
-            "        errorSupportingTextColor = errorSupportingTextColor,\n" +
-            "        focusedPrefixColor = focusedPrefixColor,\n" +
-            "        unfocusedPrefixColor = unfocusedPrefixColor,\n" +
-            "        disabledPrefixColor = disabledPrefixColor,\n" +
-            "        errorPrefixColor = errorPrefixColor,\n" +
-            "        focusedSuffixColor = focusedSuffixColor,\n" +
-            "        unfocusedSuffixColor = unfocusedSuffixColor,\n" +
-            "        disabledSuffixColor = disabledSuffixColor,\n" +
-            "        errorSuffixColor = errorSuffixColor,\n" +
-            "    )"),
-        level = DeprecationLevel.WARNING,
-    )
-    @ExperimentalMaterial3Api
-    @Composable
-    fun textFieldColors(
-        focusedTextColor: Color = FilledTextFieldTokens.FocusInputColor.value,
-        unfocusedTextColor: Color = FilledTextFieldTokens.InputColor.value,
-        disabledTextColor: Color = FilledTextFieldTokens.DisabledInputColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
-        errorTextColor: Color = FilledTextFieldTokens.ErrorInputColor.value,
-        containerColor: Color = FilledTextFieldTokens.ContainerColor.value,
-        errorContainerColor: Color = FilledTextFieldTokens.ContainerColor.value,
-        cursorColor: Color = FilledTextFieldTokens.CaretColor.value,
-        errorCursorColor: Color = FilledTextFieldTokens.ErrorFocusCaretColor.value,
-        selectionColors: TextSelectionColors = LocalTextSelectionColors.current,
-        focusedIndicatorColor: Color = FilledTextFieldTokens.FocusActiveIndicatorColor.value,
-        unfocusedIndicatorColor: Color = FilledTextFieldTokens.ActiveIndicatorColor.value,
-        disabledIndicatorColor: Color = FilledTextFieldTokens.DisabledActiveIndicatorColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledActiveIndicatorOpacity),
-        errorIndicatorColor: Color = FilledTextFieldTokens.ErrorActiveIndicatorColor.value,
-        focusedLeadingIconColor: Color = FilledTextFieldTokens.FocusLeadingIconColor.value,
-        unfocusedLeadingIconColor: Color = FilledTextFieldTokens.LeadingIconColor.value,
-        disabledLeadingIconColor: Color = FilledTextFieldTokens.DisabledLeadingIconColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledLeadingIconOpacity),
-        errorLeadingIconColor: Color = FilledTextFieldTokens.ErrorLeadingIconColor.value,
-        focusedTrailingIconColor: Color = FilledTextFieldTokens.FocusTrailingIconColor.value,
-        unfocusedTrailingIconColor: Color = FilledTextFieldTokens.TrailingIconColor.value,
-        disabledTrailingIconColor: Color = FilledTextFieldTokens.DisabledTrailingIconColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledTrailingIconOpacity),
-        errorTrailingIconColor: Color = FilledTextFieldTokens.ErrorTrailingIconColor.value,
-        focusedLabelColor: Color = FilledTextFieldTokens.FocusLabelColor.value,
-        unfocusedLabelColor: Color = FilledTextFieldTokens.LabelColor.value,
-        disabledLabelColor: Color = FilledTextFieldTokens.DisabledLabelColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledLabelOpacity),
-        errorLabelColor: Color = FilledTextFieldTokens.ErrorLabelColor.value,
-        focusedPlaceholderColor: Color = FilledTextFieldTokens.InputPlaceholderColor.value,
-        unfocusedPlaceholderColor: Color = FilledTextFieldTokens.InputPlaceholderColor.value,
-        disabledPlaceholderColor: Color = FilledTextFieldTokens.DisabledInputColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
-        errorPlaceholderColor: Color = FilledTextFieldTokens.InputPlaceholderColor.value,
-        focusedSupportingTextColor: Color = FilledTextFieldTokens.FocusSupportingColor.value,
-        unfocusedSupportingTextColor: Color = FilledTextFieldTokens.SupportingColor.value,
-        disabledSupportingTextColor: Color = FilledTextFieldTokens.DisabledSupportingColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledSupportingOpacity),
-        errorSupportingTextColor: Color = FilledTextFieldTokens.ErrorSupportingColor.value,
-        focusedPrefixColor: Color = FilledTextFieldTokens.InputPrefixColor.value,
-        unfocusedPrefixColor: Color = FilledTextFieldTokens.InputPrefixColor.value,
-        disabledPrefixColor: Color = FilledTextFieldTokens.InputPrefixColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
-        errorPrefixColor: Color = FilledTextFieldTokens.InputPrefixColor.value,
-        focusedSuffixColor: Color = FilledTextFieldTokens.InputSuffixColor.value,
-        unfocusedSuffixColor: Color = FilledTextFieldTokens.InputSuffixColor.value,
-        disabledSuffixColor: Color = FilledTextFieldTokens.InputSuffixColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
-        errorSuffixColor: Color = FilledTextFieldTokens.InputSuffixColor.value,
-    ): TextFieldColors = colors(
-        focusedTextColor = focusedTextColor,
-        unfocusedTextColor = unfocusedTextColor,
-        disabledTextColor = disabledTextColor,
-        errorTextColor = errorTextColor,
-        focusedContainerColor = containerColor,
-        unfocusedContainerColor = containerColor,
-        disabledContainerColor = containerColor,
-        errorContainerColor = errorContainerColor,
-        cursorColor = cursorColor,
-        errorCursorColor = errorCursorColor,
-        selectionColors = selectionColors,
-        focusedIndicatorColor = focusedIndicatorColor,
-        unfocusedIndicatorColor = unfocusedIndicatorColor,
-        disabledIndicatorColor = disabledIndicatorColor,
-        errorIndicatorColor = errorIndicatorColor,
-        focusedLeadingIconColor = focusedLeadingIconColor,
-        unfocusedLeadingIconColor = unfocusedLeadingIconColor,
-        disabledLeadingIconColor = disabledLeadingIconColor,
-        errorLeadingIconColor = errorLeadingIconColor,
-        focusedTrailingIconColor = focusedTrailingIconColor,
-        unfocusedTrailingIconColor = unfocusedTrailingIconColor,
-        disabledTrailingIconColor = disabledTrailingIconColor,
-        errorTrailingIconColor = errorTrailingIconColor,
-        focusedLabelColor = focusedLabelColor,
-        unfocusedLabelColor = unfocusedLabelColor,
-        disabledLabelColor = disabledLabelColor,
-        errorLabelColor = errorLabelColor,
-        focusedPlaceholderColor = focusedPlaceholderColor,
-        unfocusedPlaceholderColor = unfocusedPlaceholderColor,
-        disabledPlaceholderColor = disabledPlaceholderColor,
-        errorPlaceholderColor = errorPlaceholderColor,
-        focusedSupportingTextColor = focusedSupportingTextColor,
-        unfocusedSupportingTextColor = unfocusedSupportingTextColor,
-        disabledSupportingTextColor = disabledSupportingTextColor,
-        errorSupportingTextColor = errorSupportingTextColor,
-        focusedPrefixColor = focusedPrefixColor,
-        unfocusedPrefixColor = unfocusedPrefixColor,
-        disabledPrefixColor = disabledPrefixColor,
-        errorPrefixColor = errorPrefixColor,
-        focusedSuffixColor = focusedSuffixColor,
-        unfocusedSuffixColor = unfocusedSuffixColor,
-        disabledSuffixColor = disabledSuffixColor,
-        errorSuffixColor = errorSuffixColor,
-    )
-
-    @Deprecated(
-        message = "Renamed to `OutlinedTextFieldDefaults.colors` with additional parameters to" +
-            "control container color based on state.",
-        replaceWith = ReplaceWith("OutlinedTextFieldDefaults.colors(\n" +
-            "        focusedTextColor = focusedTextColor,\n" +
-            "        unfocusedTextColor = unfocusedTextColor,\n" +
-            "        disabledTextColor = disabledTextColor,\n" +
-            "        errorTextColor = errorTextColor,\n" +
-            "        focusedContainerColor = containerColor,\n" +
-            "        unfocusedContainerColor = containerColor,\n" +
-            "        disabledContainerColor = containerColor,\n" +
-            "        errorContainerColor = errorContainerColor,\n" +
-            "        cursorColor = cursorColor,\n" +
-            "        errorCursorColor = errorCursorColor,\n" +
-            "        selectionColors = selectionColors,\n" +
-            "        focusedBorderColor = focusedBorderColor,\n" +
-            "        unfocusedBorderColor = unfocusedBorderColor,\n" +
-            "        disabledBorderColor = disabledBorderColor,\n" +
-            "        errorBorderColor = errorBorderColor,\n" +
-            "        focusedLeadingIconColor = focusedLeadingIconColor,\n" +
-            "        unfocusedLeadingIconColor = unfocusedLeadingIconColor,\n" +
-            "        disabledLeadingIconColor = disabledLeadingIconColor,\n" +
-            "        errorLeadingIconColor = errorLeadingIconColor,\n" +
-            "        focusedTrailingIconColor = focusedTrailingIconColor,\n" +
-            "        unfocusedTrailingIconColor = unfocusedTrailingIconColor,\n" +
-            "        disabledTrailingIconColor = disabledTrailingIconColor,\n" +
-            "        errorTrailingIconColor = errorTrailingIconColor,\n" +
-            "        focusedLabelColor = focusedLabelColor,\n" +
-            "        unfocusedLabelColor = unfocusedLabelColor,\n" +
-            "        disabledLabelColor = disabledLabelColor,\n" +
-            "        errorLabelColor = errorLabelColor,\n" +
-            "        focusedPlaceholderColor = focusedPlaceholderColor,\n" +
-            "        unfocusedPlaceholderColor = unfocusedPlaceholderColor,\n" +
-            "        disabledPlaceholderColor = disabledPlaceholderColor,\n" +
-            "        errorPlaceholderColor = errorPlaceholderColor,\n" +
-            "        focusedSupportingTextColor = focusedSupportingTextColor,\n" +
-            "        unfocusedSupportingTextColor = unfocusedSupportingTextColor,\n" +
-            "        disabledSupportingTextColor = disabledSupportingTextColor,\n" +
-            "        errorSupportingTextColor = errorSupportingTextColor,\n" +
-            "        focusedPrefixColor = focusedPrefixColor,\n" +
-            "        unfocusedPrefixColor = unfocusedPrefixColor,\n" +
-            "        disabledPrefixColor = disabledPrefixColor,\n" +
-            "        errorPrefixColor = errorPrefixColor,\n" +
-            "        focusedSuffixColor = focusedSuffixColor,\n" +
-            "        unfocusedSuffixColor = unfocusedSuffixColor,\n" +
-            "        disabledSuffixColor = disabledSuffixColor,\n" +
-            "        errorSuffixColor = errorSuffixColor,\n" +
-            "    )",
-            "androidx.compose.material.OutlinedTextFieldDefaults"),
-        level = DeprecationLevel.WARNING,
-    )
-    @ExperimentalMaterial3Api
-    @Composable
-    fun outlinedTextFieldColors(
-        focusedTextColor: Color = OutlinedTextFieldTokens.FocusInputColor.value,
-        unfocusedTextColor: Color = OutlinedTextFieldTokens.InputColor.value,
-        disabledTextColor: Color = OutlinedTextFieldTokens.DisabledInputColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledInputOpacity),
-        errorTextColor: Color = OutlinedTextFieldTokens.ErrorInputColor.value,
-        containerColor: Color = Color.Transparent,
-        errorContainerColor: Color = Color.Transparent,
-        cursorColor: Color = OutlinedTextFieldTokens.CaretColor.value,
-        errorCursorColor: Color = OutlinedTextFieldTokens.ErrorFocusCaretColor.value,
-        selectionColors: TextSelectionColors = LocalTextSelectionColors.current,
-        focusedBorderColor: Color = OutlinedTextFieldTokens.FocusOutlineColor.value,
-        unfocusedBorderColor: Color = OutlinedTextFieldTokens.OutlineColor.value,
-        disabledBorderColor: Color = OutlinedTextFieldTokens.DisabledOutlineColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledOutlineOpacity),
-        errorBorderColor: Color = OutlinedTextFieldTokens.ErrorOutlineColor.value,
-        focusedLeadingIconColor: Color = OutlinedTextFieldTokens.FocusLeadingIconColor.value,
-        unfocusedLeadingIconColor: Color = OutlinedTextFieldTokens.LeadingIconColor.value,
-        disabledLeadingIconColor: Color = OutlinedTextFieldTokens.DisabledLeadingIconColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledLeadingIconOpacity),
-        errorLeadingIconColor: Color = OutlinedTextFieldTokens.ErrorLeadingIconColor.value,
-        focusedTrailingIconColor: Color = OutlinedTextFieldTokens.FocusTrailingIconColor.value,
-        unfocusedTrailingIconColor: Color = OutlinedTextFieldTokens.TrailingIconColor.value,
-        disabledTrailingIconColor: Color = OutlinedTextFieldTokens.DisabledTrailingIconColor
-            .value.copy(alpha = OutlinedTextFieldTokens.DisabledTrailingIconOpacity),
-        errorTrailingIconColor: Color = OutlinedTextFieldTokens.ErrorTrailingIconColor.value,
-        focusedLabelColor: Color = OutlinedTextFieldTokens.FocusLabelColor.value,
-        unfocusedLabelColor: Color = OutlinedTextFieldTokens.LabelColor.value,
-        disabledLabelColor: Color = OutlinedTextFieldTokens.DisabledLabelColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledLabelOpacity),
-        errorLabelColor: Color = OutlinedTextFieldTokens.ErrorLabelColor.value,
-        focusedPlaceholderColor: Color = OutlinedTextFieldTokens.InputPlaceholderColor.value,
-        unfocusedPlaceholderColor: Color = OutlinedTextFieldTokens.InputPlaceholderColor.value,
-        disabledPlaceholderColor: Color = OutlinedTextFieldTokens.DisabledInputColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledInputOpacity),
-        errorPlaceholderColor: Color = OutlinedTextFieldTokens.InputPlaceholderColor.value,
-        focusedSupportingTextColor: Color = OutlinedTextFieldTokens.FocusSupportingColor.value,
-        unfocusedSupportingTextColor: Color = OutlinedTextFieldTokens.SupportingColor.value,
-        disabledSupportingTextColor: Color = OutlinedTextFieldTokens.DisabledSupportingColor
-            .value.copy(alpha = OutlinedTextFieldTokens.DisabledSupportingOpacity),
-        errorSupportingTextColor: Color = OutlinedTextFieldTokens.ErrorSupportingColor.value,
-        focusedPrefixColor: Color = OutlinedTextFieldTokens.InputPrefixColor.value,
-        unfocusedPrefixColor: Color = OutlinedTextFieldTokens.InputPrefixColor.value,
-        disabledPrefixColor: Color = OutlinedTextFieldTokens.InputPrefixColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledInputOpacity),
-        errorPrefixColor: Color = OutlinedTextFieldTokens.InputPrefixColor.value,
-        focusedSuffixColor: Color = OutlinedTextFieldTokens.InputSuffixColor.value,
-        unfocusedSuffixColor: Color = OutlinedTextFieldTokens.InputSuffixColor.value,
-        disabledSuffixColor: Color = OutlinedTextFieldTokens.InputSuffixColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledInputOpacity),
-        errorSuffixColor: Color = OutlinedTextFieldTokens.InputSuffixColor.value,
-    ): TextFieldColors = OutlinedTextFieldDefaults.colors(
-        focusedTextColor = focusedTextColor,
-        unfocusedTextColor = unfocusedTextColor,
-        disabledTextColor = disabledTextColor,
-        errorTextColor = errorTextColor,
-        focusedContainerColor = containerColor,
-        unfocusedContainerColor = containerColor,
-        disabledContainerColor = containerColor,
-        errorContainerColor = errorContainerColor,
-        cursorColor = cursorColor,
-        errorCursorColor = errorCursorColor,
-        selectionColors = selectionColors,
-        focusedBorderColor = focusedBorderColor,
-        unfocusedBorderColor = unfocusedBorderColor,
-        disabledBorderColor = disabledBorderColor,
-        errorBorderColor = errorBorderColor,
-        focusedLeadingIconColor = focusedLeadingIconColor,
-        unfocusedLeadingIconColor = unfocusedLeadingIconColor,
-        disabledLeadingIconColor = disabledLeadingIconColor,
-        errorLeadingIconColor = errorLeadingIconColor,
-        focusedTrailingIconColor = focusedTrailingIconColor,
-        unfocusedTrailingIconColor = unfocusedTrailingIconColor,
-        disabledTrailingIconColor = disabledTrailingIconColor,
-        errorTrailingIconColor = errorTrailingIconColor,
-        focusedLabelColor = focusedLabelColor,
-        unfocusedLabelColor = unfocusedLabelColor,
-        disabledLabelColor = disabledLabelColor,
-        errorLabelColor = errorLabelColor,
-        focusedPlaceholderColor = focusedPlaceholderColor,
-        unfocusedPlaceholderColor = unfocusedPlaceholderColor,
-        disabledPlaceholderColor = disabledPlaceholderColor,
-        errorPlaceholderColor = errorPlaceholderColor,
-        focusedSupportingTextColor = focusedSupportingTextColor,
-        unfocusedSupportingTextColor = unfocusedSupportingTextColor,
-        disabledSupportingTextColor = disabledSupportingTextColor,
-        errorSupportingTextColor = errorSupportingTextColor,
-        focusedPrefixColor = focusedPrefixColor,
-        unfocusedPrefixColor = unfocusedPrefixColor,
-        disabledPrefixColor = disabledPrefixColor,
-        errorPrefixColor = errorPrefixColor,
-        focusedSuffixColor = focusedSuffixColor,
-        unfocusedSuffixColor = unfocusedSuffixColor,
-        disabledSuffixColor = disabledSuffixColor,
-        errorSuffixColor = errorSuffixColor,
-    )
-
-    @Deprecated(
-        message = "Renamed to `TextFieldDefaults.DecorationBox`",
-        replaceWith = ReplaceWith("TextFieldDefaults.DecorationBox(\n" +
-            "        value = value,\n" +
-            "        innerTextField = innerTextField,\n" +
-            "        enabled = enabled,\n" +
-            "        singleLine = singleLine,\n" +
-            "        visualTransformation = visualTransformation,\n" +
-            "        interactionSource = interactionSource,\n" +
-            "        isError = isError,\n" +
-            "        label = label,\n" +
-            "        placeholder = placeholder,\n" +
-            "        leadingIcon = leadingIcon,\n" +
-            "        trailingIcon = trailingIcon,\n" +
-            "        prefix = prefix,\n" +
-            "        suffix = suffix,\n" +
-            "        supportingText = supportingText,\n" +
-            "        shape = shape,\n" +
-            "        colors = colors,\n" +
-            "        contentPadding = contentPadding,\n" +
-            "        container = container,\n" +
-            "    )"),
-        level = DeprecationLevel.WARNING
-    )
-    @Composable
-    @ExperimentalMaterial3Api
-    fun TextFieldDecorationBox(
-        value: String,
-        innerTextField: @Composable () -> Unit,
-        enabled: Boolean,
-        singleLine: Boolean,
-        visualTransformation: VisualTransformation,
-        interactionSource: InteractionSource,
-        isError: Boolean = false,
-        label: @Composable (() -> Unit)? = null,
-        placeholder: @Composable (() -> Unit)? = null,
-        leadingIcon: @Composable (() -> Unit)? = null,
-        trailingIcon: @Composable (() -> Unit)? = null,
-        prefix: @Composable (() -> Unit)? = null,
-        suffix: @Composable (() -> Unit)? = null,
-        supportingText: @Composable (() -> Unit)? = null,
-        shape: Shape = TextFieldDefaults.shape,
-        colors: TextFieldColors = colors(),
-        contentPadding: PaddingValues =
-            if (label == null) {
-                contentPaddingWithoutLabel()
-            } else {
-                contentPaddingWithLabel()
-            },
-        container: @Composable () -> Unit = {
-            ContainerBox(enabled, isError, interactionSource, colors, shape)
-        }
-    ) = DecorationBox(
-        value = value,
-        innerTextField = innerTextField,
-        enabled = enabled,
-        singleLine = singleLine,
-        visualTransformation = visualTransformation,
-        interactionSource = interactionSource,
-        isError = isError,
-        label = label,
-        placeholder = placeholder,
-        leadingIcon = leadingIcon,
-        trailingIcon = trailingIcon,
-        prefix = prefix,
-        suffix = suffix,
-        supportingText = supportingText,
-        shape = shape,
-        colors = colors,
-        contentPadding = contentPadding,
-        container = container,
-    )
-
-    @Deprecated(
-        message = "Renamed to `OutlinedTextFieldDefaults.DecorationBox`",
-        replaceWith = ReplaceWith("OutlinedTextFieldDefaults.DecorationBox(\n" +
-            "        value = value,\n" +
-            "        innerTextField = innerTextField,\n" +
-            "        enabled = enabled,\n" +
-            "        singleLine = singleLine,\n" +
-            "        visualTransformation = visualTransformation,\n" +
-            "        interactionSource = interactionSource,\n" +
-            "        isError = isError,\n" +
-            "        label = label,\n" +
-            "        placeholder = placeholder,\n" +
-            "        leadingIcon = leadingIcon,\n" +
-            "        trailingIcon = trailingIcon,\n" +
-            "        prefix = prefix,\n" +
-            "        suffix = suffix,\n" +
-            "        supportingText = supportingText,\n" +
-            "        colors = colors,\n" +
-            "        contentPadding = contentPadding,\n" +
-            "        container = container,\n" +
-            "    )",
-            "androidx.compose.material.OutlinedTextFieldDefaults"),
-        level = DeprecationLevel.WARNING
-    )
-    @Composable
-    @ExperimentalMaterial3Api
-    fun OutlinedTextFieldDecorationBox(
-        value: String,
-        innerTextField: @Composable () -> Unit,
-        enabled: Boolean,
-        singleLine: Boolean,
-        visualTransformation: VisualTransformation,
-        interactionSource: InteractionSource,
-        isError: Boolean = false,
-        label: @Composable (() -> Unit)? = null,
-        placeholder: @Composable (() -> Unit)? = null,
-        leadingIcon: @Composable (() -> Unit)? = null,
-        trailingIcon: @Composable (() -> Unit)? = null,
-        prefix: @Composable (() -> Unit)? = null,
-        suffix: @Composable (() -> Unit)? = null,
-        supportingText: @Composable (() -> Unit)? = null,
-        colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
-        contentPadding: PaddingValues = OutlinedTextFieldDefaults.contentPadding(),
-        container: @Composable () -> Unit = {
-            OutlinedTextFieldDefaults.ContainerBox(enabled, isError, interactionSource, colors)
-        }
-    ) = OutlinedTextFieldDefaults.DecorationBox(
-        value = value,
-        innerTextField = innerTextField,
-        enabled = enabled,
-        singleLine = singleLine,
-        visualTransformation = visualTransformation,
-        interactionSource = interactionSource,
-        isError = isError,
-        label = label,
-        placeholder = placeholder,
-        leadingIcon = leadingIcon,
-        trailingIcon = trailingIcon,
-        prefix = prefix,
-        suffix = suffix,
-        supportingText = supportingText,
-        colors = colors,
-        contentPadding = contentPadding,
-        container = container,
-    )
-
-    @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
-    @ExperimentalMaterial3Api
-    @Composable
-    fun textFieldColors(
-        textColor: Color = FilledTextFieldTokens.InputColor.value,
-        disabledTextColor: Color = FilledTextFieldTokens.DisabledInputColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
-        containerColor: Color = FilledTextFieldTokens.ContainerColor.value,
-        cursorColor: Color = FilledTextFieldTokens.CaretColor.value,
-        errorCursorColor: Color = FilledTextFieldTokens.ErrorFocusCaretColor.value,
-        selectionColors: TextSelectionColors = LocalTextSelectionColors.current,
-        focusedIndicatorColor: Color = FilledTextFieldTokens.FocusActiveIndicatorColor.value,
-        unfocusedIndicatorColor: Color = FilledTextFieldTokens.ActiveIndicatorColor.value,
-        disabledIndicatorColor: Color = FilledTextFieldTokens.DisabledActiveIndicatorColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledActiveIndicatorOpacity),
-        errorIndicatorColor: Color = FilledTextFieldTokens.ErrorActiveIndicatorColor.value,
-        focusedLeadingIconColor: Color = FilledTextFieldTokens.FocusLeadingIconColor.value,
-        unfocusedLeadingIconColor: Color = FilledTextFieldTokens.LeadingIconColor.value,
-        disabledLeadingIconColor: Color = FilledTextFieldTokens.DisabledLeadingIconColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledLeadingIconOpacity),
-        errorLeadingIconColor: Color = FilledTextFieldTokens.ErrorLeadingIconColor.value,
-        focusedTrailingIconColor: Color = FilledTextFieldTokens.FocusTrailingIconColor.value,
-        unfocusedTrailingIconColor: Color = FilledTextFieldTokens.TrailingIconColor.value,
-        disabledTrailingIconColor: Color = FilledTextFieldTokens.DisabledTrailingIconColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledTrailingIconOpacity),
-        errorTrailingIconColor: Color = FilledTextFieldTokens.ErrorTrailingIconColor.value,
-        focusedLabelColor: Color = FilledTextFieldTokens.FocusLabelColor.value,
-        unfocusedLabelColor: Color = FilledTextFieldTokens.LabelColor.value,
-        disabledLabelColor: Color = FilledTextFieldTokens.DisabledLabelColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledLabelOpacity),
-        errorLabelColor: Color = FilledTextFieldTokens.ErrorLabelColor.value,
-        placeholderColor: Color = FilledTextFieldTokens.InputPlaceholderColor.value,
-        disabledPlaceholderColor: Color = FilledTextFieldTokens.DisabledInputColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
-        focusedSupportingTextColor: Color = FilledTextFieldTokens.FocusSupportingColor.value,
-        unfocusedSupportingTextColor: Color = FilledTextFieldTokens.SupportingColor.value,
-        disabledSupportingTextColor: Color = FilledTextFieldTokens.DisabledSupportingColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledSupportingOpacity),
-        errorSupportingTextColor: Color = FilledTextFieldTokens.ErrorSupportingColor.value,
-        focusedPrefixColor: Color = FilledTextFieldTokens.InputPrefixColor.value,
-        unfocusedPrefixColor: Color = FilledTextFieldTokens.InputPrefixColor.value,
-        disabledPrefixColor: Color = FilledTextFieldTokens.InputPrefixColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
-        errorPrefixColor: Color = FilledTextFieldTokens.InputPrefixColor.value,
-        focusedSuffixColor: Color = FilledTextFieldTokens.InputSuffixColor.value,
-        unfocusedSuffixColor: Color = FilledTextFieldTokens.InputSuffixColor.value,
-        disabledSuffixColor: Color = FilledTextFieldTokens.InputSuffixColor.value
-            .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
-        errorSuffixColor: Color = FilledTextFieldTokens.InputSuffixColor.value,
-    ): TextFieldColors = colors(
-        focusedTextColor = textColor,
-        unfocusedTextColor = textColor,
-        disabledTextColor = disabledTextColor,
-        errorTextColor = textColor,
-        focusedContainerColor = containerColor,
-        unfocusedContainerColor = containerColor,
-        disabledContainerColor = containerColor,
-        errorContainerColor = containerColor,
-        cursorColor = cursorColor,
-        errorCursorColor = errorCursorColor,
-        selectionColors = selectionColors,
-        focusedIndicatorColor = focusedIndicatorColor,
-        unfocusedIndicatorColor = unfocusedIndicatorColor,
-        disabledIndicatorColor = disabledIndicatorColor,
-        errorIndicatorColor = errorIndicatorColor,
-        focusedLeadingIconColor = focusedLeadingIconColor,
-        unfocusedLeadingIconColor = unfocusedLeadingIconColor,
-        disabledLeadingIconColor = disabledLeadingIconColor,
-        errorLeadingIconColor = errorLeadingIconColor,
-        focusedTrailingIconColor = focusedTrailingIconColor,
-        unfocusedTrailingIconColor = unfocusedTrailingIconColor,
-        disabledTrailingIconColor = disabledTrailingIconColor,
-        errorTrailingIconColor = errorTrailingIconColor,
-        focusedLabelColor = focusedLabelColor,
-        unfocusedLabelColor = unfocusedLabelColor,
-        disabledLabelColor = disabledLabelColor,
-        errorLabelColor = errorLabelColor,
-        focusedPlaceholderColor = placeholderColor,
-        unfocusedPlaceholderColor = placeholderColor,
-        disabledPlaceholderColor = disabledPlaceholderColor,
-        errorPlaceholderColor = placeholderColor,
-        focusedSupportingTextColor = focusedSupportingTextColor,
-        unfocusedSupportingTextColor = unfocusedSupportingTextColor,
-        disabledSupportingTextColor = disabledSupportingTextColor,
-        errorSupportingTextColor = errorSupportingTextColor,
-        focusedPrefixColor = focusedPrefixColor,
-        unfocusedPrefixColor = unfocusedPrefixColor,
-        disabledPrefixColor = disabledPrefixColor,
-        errorPrefixColor = errorPrefixColor,
-        focusedSuffixColor = focusedSuffixColor,
-        unfocusedSuffixColor = unfocusedSuffixColor,
-        disabledSuffixColor = disabledSuffixColor,
-        errorSuffixColor = errorSuffixColor,
-    )
-
-    @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
-    @ExperimentalMaterial3Api
-    @Composable
-    fun outlinedTextFieldColors(
-        textColor: Color = OutlinedTextFieldTokens.InputColor.value,
-        disabledTextColor: Color = OutlinedTextFieldTokens.DisabledInputColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledInputOpacity),
-        containerColor: Color = Color.Transparent,
-        cursorColor: Color = OutlinedTextFieldTokens.CaretColor.value,
-        errorCursorColor: Color = OutlinedTextFieldTokens.ErrorFocusCaretColor.value,
-        selectionColors: TextSelectionColors = LocalTextSelectionColors.current,
-        focusedBorderColor: Color = OutlinedTextFieldTokens.FocusOutlineColor.value,
-        unfocusedBorderColor: Color = OutlinedTextFieldTokens.OutlineColor.value,
-        disabledBorderColor: Color = OutlinedTextFieldTokens.DisabledOutlineColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledOutlineOpacity),
-        errorBorderColor: Color = OutlinedTextFieldTokens.ErrorOutlineColor.value,
-        focusedLeadingIconColor: Color = OutlinedTextFieldTokens.FocusLeadingIconColor.value,
-        unfocusedLeadingIconColor: Color = OutlinedTextFieldTokens.LeadingIconColor.value,
-        disabledLeadingIconColor: Color = OutlinedTextFieldTokens.DisabledLeadingIconColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledLeadingIconOpacity),
-        errorLeadingIconColor: Color = OutlinedTextFieldTokens.ErrorLeadingIconColor.value,
-        focusedTrailingIconColor: Color = OutlinedTextFieldTokens.FocusTrailingIconColor.value,
-        unfocusedTrailingIconColor: Color = OutlinedTextFieldTokens.TrailingIconColor.value,
-        disabledTrailingIconColor: Color = OutlinedTextFieldTokens.DisabledTrailingIconColor
-            .value.copy(alpha = OutlinedTextFieldTokens.DisabledTrailingIconOpacity),
-        errorTrailingIconColor: Color = OutlinedTextFieldTokens.ErrorTrailingIconColor.value,
-        focusedLabelColor: Color = OutlinedTextFieldTokens.FocusLabelColor.value,
-        unfocusedLabelColor: Color = OutlinedTextFieldTokens.LabelColor.value,
-        disabledLabelColor: Color = OutlinedTextFieldTokens.DisabledLabelColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledLabelOpacity),
-        errorLabelColor: Color = OutlinedTextFieldTokens.ErrorLabelColor.value,
-        placeholderColor: Color = OutlinedTextFieldTokens.InputPlaceholderColor.value,
-        disabledPlaceholderColor: Color = OutlinedTextFieldTokens.DisabledInputColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledInputOpacity),
-        focusedSupportingTextColor: Color = OutlinedTextFieldTokens.FocusSupportingColor.value,
-        unfocusedSupportingTextColor: Color = OutlinedTextFieldTokens.SupportingColor.value,
-        disabledSupportingTextColor: Color = OutlinedTextFieldTokens.DisabledSupportingColor
-            .value.copy(alpha = OutlinedTextFieldTokens.DisabledSupportingOpacity),
-        errorSupportingTextColor: Color = OutlinedTextFieldTokens.ErrorSupportingColor.value,
-        focusedPrefixColor: Color = OutlinedTextFieldTokens.InputPrefixColor.value,
-        unfocusedPrefixColor: Color = OutlinedTextFieldTokens.InputPrefixColor.value,
-        disabledPrefixColor: Color = OutlinedTextFieldTokens.InputPrefixColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledInputOpacity),
-        errorPrefixColor: Color = OutlinedTextFieldTokens.InputPrefixColor.value,
-        focusedSuffixColor: Color = OutlinedTextFieldTokens.InputSuffixColor.value,
-        unfocusedSuffixColor: Color = OutlinedTextFieldTokens.InputSuffixColor.value,
-        disabledSuffixColor: Color = OutlinedTextFieldTokens.InputSuffixColor.value
-            .copy(alpha = OutlinedTextFieldTokens.DisabledInputOpacity),
-        errorSuffixColor: Color = OutlinedTextFieldTokens.InputSuffixColor.value,
-    ): TextFieldColors = OutlinedTextFieldDefaults.colors(
-        focusedTextColor = textColor,
-        unfocusedTextColor = textColor,
-        disabledTextColor = disabledTextColor,
-        errorTextColor = textColor,
-        focusedContainerColor = containerColor,
-        unfocusedContainerColor = containerColor,
-        disabledContainerColor = containerColor,
-        errorContainerColor = containerColor,
-        cursorColor = cursorColor,
-        errorCursorColor = errorCursorColor,
-        selectionColors = selectionColors,
-        focusedBorderColor = focusedBorderColor,
-        unfocusedBorderColor = unfocusedBorderColor,
-        disabledBorderColor = disabledBorderColor,
-        errorBorderColor = errorBorderColor,
-        focusedLeadingIconColor = focusedLeadingIconColor,
-        unfocusedLeadingIconColor = unfocusedLeadingIconColor,
-        disabledLeadingIconColor = disabledLeadingIconColor,
-        errorLeadingIconColor = errorLeadingIconColor,
-        focusedTrailingIconColor = focusedTrailingIconColor,
-        unfocusedTrailingIconColor = unfocusedTrailingIconColor,
-        disabledTrailingIconColor = disabledTrailingIconColor,
-        errorTrailingIconColor = errorTrailingIconColor,
-        focusedLabelColor = focusedLabelColor,
-        unfocusedLabelColor = unfocusedLabelColor,
-        disabledLabelColor = disabledLabelColor,
-        errorLabelColor = errorLabelColor,
-        focusedPlaceholderColor = placeholderColor,
-        unfocusedPlaceholderColor = placeholderColor,
-        disabledPlaceholderColor = disabledPlaceholderColor,
-        errorPlaceholderColor = placeholderColor,
-        focusedSupportingTextColor = focusedSupportingTextColor,
-        unfocusedSupportingTextColor = unfocusedSupportingTextColor,
-        disabledSupportingTextColor = disabledSupportingTextColor,
-        errorSupportingTextColor = errorSupportingTextColor,
-        focusedPrefixColor = focusedPrefixColor,
-        unfocusedPrefixColor = unfocusedPrefixColor,
-        disabledPrefixColor = disabledPrefixColor,
-        errorPrefixColor = errorPrefixColor,
-        focusedSuffixColor = focusedSuffixColor,
-        unfocusedSuffixColor = unfocusedSuffixColor,
-        disabledSuffixColor = disabledSuffixColor,
-        errorSuffixColor = errorSuffixColor,
-    )
-
-    @Deprecated("Use overload with prefix and suffix parameters", level = DeprecationLevel.HIDDEN)
-    @Composable
-    @ExperimentalMaterial3Api
-    fun TextFieldDecorationBox(
-        value: String,
-        innerTextField: @Composable () -> Unit,
-        enabled: Boolean,
-        singleLine: Boolean,
-        visualTransformation: VisualTransformation,
-        interactionSource: InteractionSource,
-        isError: Boolean = false,
-        label: @Composable (() -> Unit)? = null,
-        placeholder: @Composable (() -> Unit)? = null,
-        leadingIcon: @Composable (() -> Unit)? = null,
-        trailingIcon: @Composable (() -> Unit)? = null,
-        supportingText: @Composable (() -> Unit)? = null,
-        shape: Shape = TextFieldDefaults.shape,
-        colors: TextFieldColors = colors(),
-        contentPadding: PaddingValues =
-            if (label == null) {
-                contentPaddingWithoutLabel()
-            } else {
-                contentPaddingWithLabel()
-            },
-        container: @Composable () -> Unit = {
-            ContainerBox(enabled, isError, interactionSource, colors, shape)
-        }
-    ) {
-        DecorationBox(
-            value = value,
-            innerTextField = innerTextField,
-            enabled = enabled,
-            singleLine = singleLine,
-            visualTransformation = visualTransformation,
-            interactionSource = interactionSource,
-            isError = isError,
-            label = label,
-            placeholder = placeholder,
-            leadingIcon = leadingIcon,
-            trailingIcon = trailingIcon,
-            prefix = null,
-            suffix = null,
-            supportingText = supportingText,
-            shape = shape,
-            colors = colors,
-            contentPadding = contentPadding,
-            container = container,
-        )
-    }
-
-    @Deprecated("Use overload with prefix and suffix parameters", level = DeprecationLevel.HIDDEN)
-    @Composable
-    @ExperimentalMaterial3Api
-    fun OutlinedTextFieldDecorationBox(
-        value: String,
-        innerTextField: @Composable () -> Unit,
-        enabled: Boolean,
-        singleLine: Boolean,
-        visualTransformation: VisualTransformation,
-        interactionSource: InteractionSource,
-        isError: Boolean = false,
-        label: @Composable (() -> Unit)? = null,
-        placeholder: @Composable (() -> Unit)? = null,
-        leadingIcon: @Composable (() -> Unit)? = null,
-        trailingIcon: @Composable (() -> Unit)? = null,
-        supportingText: @Composable (() -> Unit)? = null,
-        colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
-        contentPadding: PaddingValues = OutlinedTextFieldDefaults.contentPadding(),
-        container: @Composable () -> Unit = {
-            OutlinedTextFieldDefaults.ContainerBox(enabled, isError, interactionSource, colors)
-        }
-    ) {
-        OutlinedTextFieldDefaults.DecorationBox(
-            value = value,
-            innerTextField = innerTextField,
-            enabled = enabled,
-            singleLine = singleLine,
-            visualTransformation = visualTransformation,
-            interactionSource = interactionSource,
-            isError = isError,
-            label = label,
-            placeholder = placeholder,
-            leadingIcon = leadingIcon,
-            trailingIcon = trailingIcon,
-            prefix = null,
-            suffix = null,
-            supportingText = supportingText,
-            colors = colors,
-            contentPadding = contentPadding,
-            container = container
-        )
-    }
 }
 
 /**
@@ -1694,8 +912,9 @@
                 focusedSupportingTextColor =
                 fromToken(OutlinedTextFieldTokens.FocusSupportingColor),
                 unfocusedSupportingTextColor = fromToken(OutlinedTextFieldTokens.SupportingColor),
-                disabledSupportingTextColor = OutlinedTextFieldTokens.DisabledSupportingColor
-                    .value.copy(alpha = OutlinedTextFieldTokens.DisabledSupportingOpacity),
+                disabledSupportingTextColor =
+                fromToken(OutlinedTextFieldTokens.DisabledSupportingColor)
+                    .copy(alpha = OutlinedTextFieldTokens.DisabledSupportingOpacity),
                 errorSupportingTextColor = fromToken(OutlinedTextFieldTokens.ErrorSupportingColor),
                 focusedPrefixColor = fromToken(OutlinedTextFieldTokens.InputPrefixColor),
                 unfocusedPrefixColor = fromToken(OutlinedTextFieldTokens.InputPrefixColor),
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Arrangement.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Arrangement.kt
index d410938..11f3179 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Arrangement.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Arrangement.kt
@@ -84,6 +84,7 @@
          * fitting the arrangement to the size of the carousel.
          *
          * @param availableSpace the space the arrangement needs to fit
+         * @param itemSpacing the space between items in the arrangement
          * @param targetSmallSize the size small items would like to be
          * @param minSmallSize the minimum size of which small item sizes are allowed to be
          * @param maxSmallSize the maximum size of which small item sizes are allowed to be
@@ -100,6 +101,7 @@
          */
         fun findLowestCostArrangement(
             availableSpace: Float,
+            itemSpacing: Float,
             targetSmallSize: Float,
             minSmallSize: Float,
             maxSmallSize: Float,
@@ -117,6 +119,7 @@
                         val arrangement = fit(
                             priority = priority,
                             availableSpace = availableSpace,
+                            itemSpacing = itemSpacing,
                             smallCount = smallCount,
                             smallSize = targetSmallSize,
                             minSmallSize = minSmallSize,
@@ -155,8 +158,9 @@
          * adjusting small items as much as possible, then adjusting medium items as much as
          * possible, and finally adjusting large items if the arrangement is still unable to fit.
          *
-         * @param priority The priority to place on this particular arrangement of item counts
-         * @param availableSpace The space in which to fit the arrangement
+         * @param priority the priority to place on this particular arrangement of item counts
+         * @param availableSpace the space in which to fit the arrangement
+         * @param itemSpacing the space between itens
          * @param smallCount the number of small items to fit
          * @param smallSize the size of each small item
          * @param minSmallSize the minimum size a small item is allowed to be
@@ -170,6 +174,7 @@
         private fun fit(
             priority: Int,
             availableSpace: Float,
+            itemSpacing: Float,
             smallCount: Int,
             smallSize: Float,
             minSmallSize: Float,
@@ -179,6 +184,8 @@
             largeCount: Int,
             largeSize: Float
         ): Arrangement {
+            val totalItemCount = largeCount + mediumCount + smallCount
+            val availableSpaceWithoutSpacing = availableSpace - ((totalItemCount - 1) * itemSpacing)
             var arrangedSmallSize = smallSize.coerceIn(
                 minSmallSize,
                 maxSmallSize
@@ -188,7 +195,7 @@
 
             val totalSpaceTakenByArrangement = arrangedLargeSize * largeCount +
                 arrangedMediumSize * mediumCount + arrangedSmallSize * smallCount
-            val delta = availableSpace - totalSpaceTakenByArrangement
+            val delta = availableSpaceWithoutSpacing - totalSpaceTakenByArrangement
             // First, resize small items within their allowable min-max range to try to fit the
             // arrangement into the available space.
             if (smallCount > 0 && delta > 0) {
@@ -208,7 +215,7 @@
             // Zero out small size if there are no small items
             arrangedSmallSize = if (smallCount > 0) arrangedSmallSize else 0f
             arrangedLargeSize = calculateLargeSize(
-                availableSpace, smallCount, arrangedSmallSize,
+                availableSpaceWithoutSpacing, smallCount, arrangedSmallSize,
                 mediumCount, largeCount
             )
             arrangedMediumSize = (arrangedLargeSize + arrangedSmallSize) / 2f
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index b34fc8f..8eb1984 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -16,8 +16,8 @@
 
 package androidx.compose.material3.carousel
 
+import androidx.annotation.VisibleForTesting
 import androidx.collection.IntIntMap
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.TargetedFlingBehavior
 import androidx.compose.foundation.layout.Box
@@ -45,6 +45,9 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFilter
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.ceil
 import kotlin.math.roundToInt
 
 /**
@@ -88,14 +91,14 @@
     Carousel(
         state = state,
         orientation = Orientation.Horizontal,
-        keylineList = { availableSpace ->
+        keylineList = { availableSpace, itemSpacingPx ->
             with(density) {
                 multiBrowseKeylineList(
                     density = this,
                     carouselMainAxisSize = availableSpace,
                     preferredItemSize = preferredItemSize.toPx(),
-                    itemSpacing = itemSpacing.toPx(),
                     itemCount = state.itemCountState.value.invoke(),
+                    itemSpacing = itemSpacingPx,
                     minSmallSize = minSmallSize.toPx(),
                     maxSmallSize = maxSmallSize.toPx(),
                 )
@@ -141,13 +144,13 @@
     Carousel(
         state = state,
         orientation = Orientation.Horizontal,
-        keylineList = {
+        keylineList = { availableSpace, itemSpacingPx ->
             with(density) {
                 uncontainedKeylineList(
                     density = this,
-                    carouselMainAxisSize = state.pagerState.layoutInfo.viewportSize.width.toFloat(),
+                    carouselMainAxisSize = availableSpace,
                     itemSize = itemSize.toPx(),
-                    itemSpacing = itemSpacing.toPx(),
+                    itemSpacing = itemSpacingPx,
                 )
             }
         },
@@ -179,7 +182,7 @@
 internal fun Carousel(
     state: CarouselState,
     orientation: Orientation,
-    keylineList: (availableSpace: Float) -> KeylineList?,
+    keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?,
     modifier: Modifier = Modifier,
     itemSpacing: Dp = 0.dp,
     flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state.pagerState),
@@ -188,8 +191,9 @@
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
     val pageSize = remember(keylineList) { CarouselPageSize(keylineList) }
 
-    // TODO: Update beyond bounds numbers according to Strategy
-    val outOfBoundsPageCount = 2
+    val outOfBoundsPageCount = remember(pageSize.strategy.itemMainAxisSize) {
+        calculateOutOfBounds(pageSize.strategy)
+    }
     val carouselScope = CarouselScopeImpl
 
     val snapPositionMap = remember(pageSize.strategy.itemMainAxisSize) {
@@ -247,6 +251,23 @@
     }
 }
 
+internal fun calculateOutOfBounds(strategy: Strategy): Int {
+    if (!strategy.isValid()) {
+        return PagerDefaults.OutOfBoundsPageCount
+    }
+    var totalKeylineSpace = 0f
+    var totalNonAnchorKeylines = 0
+    strategy.defaultKeylines.fastFilter { !it.isAnchor }.fastForEach {
+        totalKeylineSpace += it.size
+        totalNonAnchorKeylines += 1
+    }
+    val itemsLoaded = ceil(totalKeylineSpace / strategy.itemMainAxisSize).toInt()
+    val itemsToLoad = totalNonAnchorKeylines - itemsLoaded
+
+    // We must also load the next item when scrolling
+    return itemsToLoad + 1
+}
+
 /**
  * A [PageSize] implementation that maintains a strategy that is kept up-to-date with the
  * latest available space of the container.
@@ -254,10 +275,12 @@
  * @param keylineList The list of keylines that are fixed positions along the scrolling axis which
  * define the state an item should be in when its center is co-located with the keyline's position.
  */
-private class CarouselPageSize(keylineList: (availableSpace: Float) -> KeylineList?) : PageSize {
+private class CarouselPageSize(
+    keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?
+) : PageSize {
     val strategy = Strategy(keylineList)
     override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
-        strategy.apply(availableSpace.toFloat())
+        strategy.apply(availableSpace.toFloat(), pageSpacing.toFloat())
         return if (strategy.isValid()) {
             strategy.itemMainAxisSize.roundToInt()
         } else {
@@ -295,7 +318,7 @@
  * @param itemPositionMap the position of each index when it is the current item
  * @param isRtl whether or not the carousel is rtl
  */
-@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class)
 internal fun Modifier.carouselItem(
     index: Int,
     state: CarouselState,
@@ -303,17 +326,8 @@
     itemPositionMap: IntIntMap,
     isRtl: Boolean
 ): Modifier {
-    val viewportSize = state.pagerState.layoutInfo.viewportSize
-    val orientation = state.pagerState.layoutInfo.orientation
-    val isVertical = orientation == Orientation.Vertical
-    val mainAxisCarouselSize = if (isVertical) viewportSize.height else viewportSize.width
-
-    if (mainAxisCarouselSize == 0 || !strategy.isValid()) {
-        return this
-    }
-    val itemsCount = state.pagerState.pageCount
-    val maxScrollOffset =
-        itemsCount * strategy.itemMainAxisSize - mainAxisCarouselSize
+    if (!strategy.isValid()) return this
+    val isVertical = state.pagerState.layoutInfo.orientation == Orientation.Vertical
 
     return layout { measurable, constraints ->
         // Force the item to use the strategy's itemMainAxisSize along its main axis
@@ -339,16 +353,15 @@
             placeable.place(0, 0)
         }
     }.graphicsLayer {
-        val currentItemScrollOffset =
-            (state.pagerState.currentPage * strategy.itemMainAxisSize) +
-                (state.pagerState.currentPageOffsetFraction * strategy.itemMainAxisSize)
-        val scrollOffset = currentItemScrollOffset -
-            (if (itemPositionMap.size > 0) itemPositionMap[state.pagerState.currentPage] else 0)
+        val scrollOffset = calculateCurrentScrollOffset(state, strategy, itemPositionMap)
+        val maxScrollOffset = calculateMaxScrollOffset(state, strategy)
+        // TODO: Reduce the number of times a keyline for the same scroll offset is calculated
         val keylines = strategy.getKeylineListForScrollOffset(scrollOffset, maxScrollOffset)
 
         // Find center of the item at this index
+        val itemSizeWithSpacing = strategy.itemMainAxisSize + strategy.itemSpacing
         val unadjustedCenter =
-            (index * strategy.itemMainAxisSize) + (strategy.itemMainAxisSize / 2f) - scrollOffset
+            (index * itemSizeWithSpacing) + (strategy.itemMainAxisSize / 2f) - scrollOffset
 
         // Find the keyline before and after this item's center and create an interpolated
         // keyline that the item should use for its clip shape and offset
@@ -364,6 +377,7 @@
         clip = true
         shape = object : Shape {
             // TODO: Find a way to use the shape of the item set by the client for each item
+            // TODO: Allow corner size customization
             val roundedCornerShape = RoundedCornerShape(ShapeDefaults.ExtraLarge.topStart)
             override fun createOutline(
                 size: Size,
@@ -420,6 +434,32 @@
     }
 }
 
+/** Calculates the current scroll offset given item count, sizing, spacing, and snap position. */
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun calculateCurrentScrollOffset(
+    state: CarouselState,
+    strategy: Strategy,
+    snapPositionMap: IntIntMap
+): Float {
+    val itemSizeWithSpacing = strategy.itemMainAxisSize + strategy.itemSpacing
+    val currentItemScrollOffset =
+        (state.pagerState.currentPage * itemSizeWithSpacing) +
+            (state.pagerState.currentPageOffsetFraction * itemSizeWithSpacing)
+    return currentItemScrollOffset -
+        (if (snapPositionMap.size > 0) snapPositionMap[state.pagerState.currentPage] else 0)
+}
+
+/** Returns the max scroll offset given the item count, sizing, and spacing. */
+@OptIn(ExperimentalMaterial3Api::class)
+@VisibleForTesting
+internal fun calculateMaxScrollOffset(state: CarouselState, strategy: Strategy): Float {
+    val itemCount = state.pagerState.pageCount.toFloat()
+    val maxScrollPossible = (strategy.itemMainAxisSize * itemCount) +
+        (strategy.itemSpacing * (itemCount - 1))
+
+    return (maxScrollPossible - strategy.availableSpace).coerceAtLeast(0f)
+}
+
 /**
  * Returns a float between 0 and 1 that represents how far [unadjustedOffset] is between
  * [before] and [after].
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineList.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineList.kt
index 88a1ecc..a0125af 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineList.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineList.kt
@@ -229,12 +229,17 @@
  */
 internal fun keylineListOf(
     carouselMainAxisSize: Float,
+    itemSpacing: Float,
     carouselAlignment: CarouselAlignment,
     keylines: KeylineListScope.() -> Unit
 ): KeylineList {
     val keylineListScope = KeylineListScopeImpl()
     keylines.invoke(keylineListScope)
-    return keylineListScope.createWithAlignment(carouselMainAxisSize, carouselAlignment)
+    return keylineListScope.createWithAlignment(
+        carouselMainAxisSize,
+        itemSpacing,
+        carouselAlignment
+    )
 }
 
 /**
@@ -243,13 +248,19 @@
  */
 internal fun keylineListOf(
     carouselMainAxisSize: Float,
+    itemSpacing: Float,
     pivotIndex: Int,
     pivotOffset: Float,
     keylines: KeylineListScope.() -> Unit
 ): KeylineList {
     val keylineListScope = KeylineListScopeImpl()
     keylines.invoke(keylineListScope)
-    return keylineListScope.createWithPivot(carouselMainAxisSize, pivotIndex, pivotOffset)
+    return keylineListScope.createWithPivot(
+        carouselMainAxisSize,
+        itemSpacing,
+        pivotIndex,
+        pivotOffset
+    )
 }
 
 /** Receiver scope for creating a [KeylineList] using [keylineListOf] */
@@ -290,6 +301,7 @@
 
     fun createWithPivot(
         carouselMainAxisSize: Float,
+        itemSpacing: Float,
         pivotIndex: Int,
         pivotOffset: Float
     ): KeylineList {
@@ -300,6 +312,7 @@
             findLastFocalIndex(),
             itemMainAxisSize = focalItemSize,
             carouselMainAxisSize = carouselMainAxisSize,
+            itemSpacing,
             tmpKeylines
         )
         return KeylineList(keylines)
@@ -307,6 +320,7 @@
 
     fun createWithAlignment(
         carouselMainAxisSize: Float,
+        itemSpacing: Float,
         carouselAlignment: CarouselAlignment
     ): KeylineList {
         val lastFocalIndex = findLastFocalIndex()
@@ -315,7 +329,16 @@
         pivotIndex = firstFocalIndex
         pivotOffset = when (carouselAlignment) {
             CarouselAlignment.Center -> {
-                (carouselMainAxisSize / 2) - ((focalItemSize / 2) * focalItemCount)
+                // If there is an even number of keylines, the itemSpacing will be placed in the
+                // center of the container. Divide the item spacing by half before subtracting
+                // the pivot item's center.
+                val itemSpacingSplit = if (itemSpacing == 0f || focalItemCount.mod(2) == 0) {
+                    0f
+                } else {
+                    itemSpacing / 2f
+                }
+                (carouselMainAxisSize / 2) - ((focalItemSize / 2) * focalItemCount) -
+                    itemSpacingSplit
             }
             CarouselAlignment.End -> carouselMainAxisSize - (focalItemSize / 2)
             // Else covers and defaults to CarouselAlignment.Start
@@ -329,6 +352,7 @@
             lastFocalIndex,
             itemMainAxisSize = focalItemSize,
             carouselMainAxisSize = carouselMainAxisSize,
+            itemSpacing,
             tmpKeylines
         )
         return KeylineList(keylines)
@@ -375,6 +399,7 @@
         lastFocalIndex: Int,
         itemMainAxisSize: Float,
         carouselMainAxisSize: Float,
+        itemSpacing: Float,
         tmpKeylines: List<TmpKeyline>
     ): List<Keyline> {
         val pivot = tmpKeylines[pivotIndex]
@@ -406,8 +431,8 @@
         // Convert all TmpKeylines before the pivot to Keylines by calculating their offset,
         // unadjustedOffset, and cutoff and insert them at the beginning of the keyline list,
         // maintaining the tmpKeyline list's original order.
-        var offset = pivotOffset - (itemMainAxisSize / 2)
-        var unadjustedOffset = pivotOffset - (itemMainAxisSize / 2)
+        var offset = pivotOffset - (itemMainAxisSize / 2) - itemSpacing
+        var unadjustedOffset = pivotOffset - (itemMainAxisSize / 2) - itemSpacing
         (pivotIndex - 1 downTo 0).forEach { originalIndex ->
             val tmp = tmpKeylines[originalIndex]
             val tmpOffset = offset - (tmp.size / 2)
@@ -426,15 +451,15 @@
                 )
             )
 
-            offset -= tmp.size
-            unadjustedOffset -= itemMainAxisSize
+            offset -= tmp.size + itemSpacing
+            unadjustedOffset -= itemMainAxisSize + itemSpacing
         }
 
         // Convert all TmpKeylines after the pivot to Keylines by calculating their offset,
         // unadjustedOffset, and cutoff and inserting them at the end of the keyline list,
         // maintaining the tmpKeyline list's original order.
-        offset = pivotOffset + (itemMainAxisSize / 2)
-        unadjustedOffset = pivotOffset + (itemMainAxisSize / 2)
+        offset = pivotOffset + (itemMainAxisSize / 2) + itemSpacing
+        unadjustedOffset = pivotOffset + (itemMainAxisSize / 2) + itemSpacing
         (pivotIndex + 1 until tmpKeylines.size).forEach { originalIndex ->
             val tmp = tmpKeylines[originalIndex]
             val tmpOffset = offset + (tmp.size / 2)
@@ -456,8 +481,8 @@
                 )
             )
 
-            offset += tmp.size
-            unadjustedOffset += itemMainAxisSize
+            offset += tmp.size + itemSpacing
+            unadjustedOffset += itemMainAxisSize + itemSpacing
         }
 
         return keylines
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
index 0ddfab3..d7f04c8 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
@@ -58,15 +58,12 @@
     var smallCounts: IntArray = intArrayOf(1)
     val mediumCounts: IntArray = intArrayOf(1, 0)
 
-    val targetLargeSize: Float = min(preferredItemSize + itemSpacing, carouselMainAxisSize)
+    val targetLargeSize: Float = min(preferredItemSize, carouselMainAxisSize)
     // Ideally we would like to create a balanced arrangement where a small item is 1/3 the size
     // of the large item and medium items are sized between large and small items. Clamp the
     // small target size within our min-max range and as close to 1/3 of the target large item
     // size as possible.
-    val targetSmallSize: Float = (targetLargeSize / 3f + itemSpacing).coerceIn(
-        minSmallSize + itemSpacing,
-        maxSmallSize + itemSpacing
-    )
+    val targetSmallSize: Float = (targetLargeSize / 3f).coerceIn(minSmallSize, maxSmallSize)
     val targetMediumSize = (targetLargeSize + targetSmallSize) / 2f
 
     if (carouselMainAxisSize < minSmallSize * 2) {
@@ -89,6 +86,7 @@
     val anchorSize = with(density) { StrategyDefaults.AnchorSize.toPx() }
     var arrangement = Arrangement.findLowestCostArrangement(
         availableSpace = carouselMainAxisSize,
+        itemSpacing = itemSpacing,
         targetSmallSize = targetSmallSize,
         minSmallSize = minSmallSize,
         maxSmallSize = maxSmallSize,
@@ -117,6 +115,7 @@
         }
         arrangement = Arrangement.findLowestCostArrangement(
             availableSpace = carouselMainAxisSize,
+            itemSpacing = itemSpacing,
             targetSmallSize = targetSmallSize,
             minSmallSize = minSmallSize,
             maxSmallSize = maxSmallSize,
@@ -134,6 +133,7 @@
 
     return createLeftAlignedKeylineList(
         carouselMainAxisSize = carouselMainAxisSize,
+        itemSpacing = itemSpacing,
         rightAnchorSize = anchorSize,
         leftAnchorSize = anchorSize,
         arrangement = arrangement
@@ -142,11 +142,12 @@
 
 internal fun createLeftAlignedKeylineList(
     carouselMainAxisSize: Float,
+    itemSpacing: Float,
     leftAnchorSize: Float,
     rightAnchorSize: Float,
     arrangement: Arrangement
 ): KeylineList {
-    return keylineListOf(carouselMainAxisSize, CarouselAlignment.Start) {
+    return keylineListOf(carouselMainAxisSize, itemSpacing, CarouselAlignment.Start) {
         add(leftAnchorSize, isAnchor = true)
 
         repeat(arrangement.largeCount) { add(arrangement.largeSize) }
@@ -209,9 +210,11 @@
     val leftAnchorSize: Float = max(xSmallSize, mediumItemSize * 0.5f)
     return createLeftAlignedKeylineList(
         carouselMainAxisSize = carouselMainAxisSize,
+        itemSpacing = itemSpacing,
         leftAnchorSize = leftAnchorSize,
         rightAnchorSize = defaultAnchorSize,
-        arrangement = arrangement)
+        arrangement = arrangement
+    )
 }
 
 /**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
index 0048b14..b7b5d53 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
@@ -59,7 +59,7 @@
  * carousel's available space. This function will be called anytime availableSpace changes.
  */
 internal class Strategy(
-    private val keylineList: (availableSpace: Float) -> KeylineList?
+    private val keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?
 ) {
 
     /** The keylines generated from the [keylineList] block. */
@@ -92,7 +92,9 @@
     private lateinit var endShiftPoints: FloatList
 
     /** The available space in the main axis used in the most recent call to [apply]. */
-    private var availableSpace: Float = 0f
+    internal var availableSpace: Float = 0f
+    /** The spacing between each item. */
+    internal var itemSpacing: Float = 0f
     /** The size of items when in focus and fully unmasked. */
     internal var itemMainAxisSize by mutableFloatStateOf(0f)
 
@@ -111,17 +113,17 @@
      *
      * @param availableSpace the size of the carousel container in scrolling axis
      */
-    internal fun apply(availableSpace: Float): Strategy {
+    internal fun apply(availableSpace: Float, itemSpacing: Float): Strategy {
         // Skip computing new keylines and updating this strategy if
         // available space has not changed.
-        if (this.availableSpace == availableSpace) {
+        if (this.availableSpace == availableSpace && this.itemSpacing == itemSpacing) {
             return this
         }
 
-        val keylineList = keylineList.invoke(availableSpace) ?: return this
-        val startKeylineSteps = getStartKeylineSteps(keylineList, availableSpace)
+        val keylineList = keylineList.invoke(availableSpace, itemSpacing) ?: return this
+        val startKeylineSteps = getStartKeylineSteps(keylineList, availableSpace, itemSpacing)
         val endKeylineSteps =
-            getEndKeylineSteps(keylineList, availableSpace)
+            getEndKeylineSteps(keylineList, availableSpace, itemSpacing)
 
         // TODO: Update this to use the first/last focal keylines to calculate shift?
         val startShiftDistance = startKeylineSteps.last().first().unadjustedOffset -
@@ -146,6 +148,7 @@
             false
         )
         this.availableSpace = availableSpace
+        this.itemSpacing = itemSpacing
         this.itemMainAxisSize = defaultKeylines.firstFocal.size
 
         return this
@@ -236,6 +239,7 @@
 
         if (isValid() != other.isValid()) return false
         if (availableSpace != other.availableSpace) return false
+        if (itemSpacing != other.itemSpacing) return false
         if (itemMainAxisSize != other.itemMainAxisSize) return false
         if (startShiftDistance != other.startShiftDistance) return false
         if (endShiftDistance != other.endShiftDistance) return false
@@ -253,6 +257,7 @@
 
         var result = isValid().hashCode()
         result = 31 * result + availableSpace.hashCode()
+        result = 31 * result + itemSpacing.hashCode()
         result = 31 * result + itemMainAxisSize.hashCode()
         result = 31 * result + startShiftDistance.hashCode()
         result = 31 * result + endShiftDistance.hashCode()
@@ -282,7 +287,8 @@
          */
         private fun getStartKeylineSteps(
             defaultKeylines: KeylineList,
-            carouselMainAxisSize: Float
+            carouselMainAxisSize: Float,
+            itemSpacing: Float
         ): List<KeylineList> {
             val steps: MutableList<KeylineList> = mutableListOf()
             steps.add(defaultKeylines)
@@ -303,7 +309,8 @@
                         from = defaultKeylines,
                         srcIndex = 0,
                         dstIndex = 0,
-                        carouselMainAxisSize = carouselMainAxisSize
+                        carouselMainAxisSize = carouselMainAxisSize,
+                        itemSpacing = itemSpacing
                     )
                 )
                 return steps
@@ -326,7 +333,8 @@
                         from = prevStep,
                         srcIndex = defaultKeylines.firstNonAnchorIndex,
                         dstIndex = dstIndex,
-                        carouselMainAxisSize = carouselMainAxisSize
+                        carouselMainAxisSize = carouselMainAxisSize,
+                        itemSpacing = itemSpacing
                     )
                 )
                 i++
@@ -353,7 +361,8 @@
          */
         private fun getEndKeylineSteps(
             defaultKeylines: KeylineList,
-            carouselMainAxisSize: Float
+            carouselMainAxisSize: Float,
+            itemSpacing: Float
         ): List<KeylineList> {
             val steps: MutableList<KeylineList> = mutableListOf()
             steps.add(defaultKeylines)
@@ -374,7 +383,8 @@
                         from = defaultKeylines,
                         srcIndex = 0,
                         dstIndex = 0,
-                        carouselMainAxisSize = carouselMainAxisSize
+                        carouselMainAxisSize = carouselMainAxisSize,
+                        itemSpacing = itemSpacing
                     )
                 )
                 return steps
@@ -397,7 +407,8 @@
                     from = prevStep,
                     srcIndex = defaultKeylines.lastNonAnchorIndex,
                     dstIndex = dstIndex,
-                    carouselMainAxisSize = carouselMainAxisSize
+                    carouselMainAxisSize = carouselMainAxisSize,
+                    itemSpacing = itemSpacing
                 )
                 steps.add(keylines)
                 i++
@@ -414,14 +425,15 @@
             from: KeylineList,
             srcIndex: Int,
             dstIndex: Int,
-            carouselMainAxisSize: Float
+            carouselMainAxisSize: Float,
+            itemSpacing: Float
         ): KeylineList {
             // -1 if the pivot is shifting left/top, 1 if shifting right/bottom
             val pivotDir = if (srcIndex > dstIndex) 1 else -1
-            val pivotDelta = (from[srcIndex].size - from[srcIndex].cutoff) * pivotDir
+            val pivotDelta = (from[srcIndex].size - from[srcIndex].cutoff + itemSpacing) * pivotDir
             val newPivotIndex = from.pivotIndex + pivotDir
             val newPivotOffset = from.pivot.offset + pivotDelta
-            return keylineListOf(carouselMainAxisSize, newPivotIndex, newPivotOffset) {
+            return keylineListOf(carouselMainAxisSize, itemSpacing, newPivotIndex, newPivotOffset) {
                 from.toMutableList()
                     .move(srcIndex, dstIndex)
                     .fastForEach { k -> add(k.size, k.isAnchor) }
diff --git a/compose/runtime/runtime-livedata/src/androidTest/java/androidx/compose/runtime/livedata/LiveDataAdapterTest.kt b/compose/runtime/runtime-livedata/src/androidTest/java/androidx/compose/runtime/livedata/LiveDataAdapterTest.kt
index 9733e52..756ccab 100644
--- a/compose/runtime/runtime-livedata/src/androidTest/java/androidx/compose/runtime/livedata/LiveDataAdapterTest.kt
+++ b/compose/runtime/runtime-livedata/src/androidTest/java/androidx/compose/runtime/livedata/LiveDataAdapterTest.kt
@@ -20,10 +20,10 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index fe7f66b..886c585 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -723,7 +723,7 @@
 
 package androidx.compose.runtime.internal {
 
-  @androidx.compose.runtime.ComposeCompilerApi @androidx.compose.runtime.Stable public interface ComposableLambda extends kotlin.jvm.functions.Function2<androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function10<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function11<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function13<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function14<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function15<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function16<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function17<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function18<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function19<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function20<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function21<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function3<java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function4<java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function5<java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function6<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function7<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function8<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function9<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> {
+  @androidx.compose.runtime.ComposeCompilerApi @androidx.compose.runtime.Stable public interface ComposableLambda extends kotlin.jvm.functions.Function2<androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function10<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function11<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function13<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function14<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function15<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function16<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function17<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function18<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function19<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function20<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function21<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function3<java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function4<java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function5<java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function6<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function7<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function8<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function9<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> {
   }
 
   public final class ComposableLambdaKt {
@@ -732,7 +732,7 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposeCompilerApi public static androidx.compose.runtime.internal.ComposableLambda rememberComposableLambda(int key, boolean tracked, Object block);
   }
 
-  @androidx.compose.runtime.ComposeCompilerApi @androidx.compose.runtime.Stable public interface ComposableLambdaN extends kotlin.jvm.functions.FunctionN<java.lang.Object> {
+  @androidx.compose.runtime.ComposeCompilerApi @androidx.compose.runtime.Stable public interface ComposableLambdaN extends kotlin.jvm.functions.FunctionN<java.lang.Object?> {
   }
 
   public final class ComposableLambdaN_jvmKt {
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 63348ec..d282011 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -758,7 +758,7 @@
 
 package androidx.compose.runtime.internal {
 
-  @androidx.compose.runtime.ComposeCompilerApi @androidx.compose.runtime.Stable public interface ComposableLambda extends kotlin.jvm.functions.Function2<androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function10<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function11<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function13<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function14<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function15<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function16<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function17<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function18<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function19<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function20<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function21<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function3<java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function4<java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function5<java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function6<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function7<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function8<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> kotlin.jvm.functions.Function9<java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object> {
+  @androidx.compose.runtime.ComposeCompilerApi @androidx.compose.runtime.Stable public interface ComposableLambda extends kotlin.jvm.functions.Function2<androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function10<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function11<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function13<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function14<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function15<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function16<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function17<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function18<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function19<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function20<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function21<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function3<java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function4<java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function5<java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function6<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function7<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function8<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> kotlin.jvm.functions.Function9<java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,java.lang.Object?,androidx.compose.runtime.Composer,java.lang.Integer,java.lang.Object?> {
   }
 
   public final class ComposableLambdaKt {
@@ -767,7 +767,7 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposeCompilerApi public static androidx.compose.runtime.internal.ComposableLambda rememberComposableLambda(int key, boolean tracked, Object block);
   }
 
-  @androidx.compose.runtime.ComposeCompilerApi @androidx.compose.runtime.Stable public interface ComposableLambdaN extends kotlin.jvm.functions.FunctionN<java.lang.Object> {
+  @androidx.compose.runtime.ComposeCompilerApi @androidx.compose.runtime.Stable public interface ComposableLambdaN extends kotlin.jvm.functions.FunctionN<java.lang.Object?> {
   }
 
   public final class ComposableLambdaN_jvmKt {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
index 1718d92..df5f37a 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
@@ -48,10 +48,14 @@
     private var awaiters = mutableListOf<FrameAwaiter<*>>()
     private var spareList = mutableListOf<FrameAwaiter<*>>()
 
+    // Uses AtomicInt to avoid adding AtomicBoolean to the Expect/Actual requirements of the
+    // runtime.
+    private val hasAwaitersUnlocked = AtomicInt(0)
+
     /**
      * `true` if there are any callers of [withFrameNanos] awaiting to run for a pending frame.
      */
-    val hasAwaiters: Boolean get() = synchronized(lock) { awaiters.isNotEmpty() }
+    val hasAwaiters: Boolean get() = hasAwaitersUnlocked.get() != 0
 
     /**
      * Send a frame for time [timeNanos] to all current callers of [withFrameNanos].
@@ -66,6 +70,7 @@
             val toResume = awaiters
             awaiters = spareList
             spareList = toResume
+            hasAwaitersUnlocked.set(0)
 
             for (i in 0 until toResume.size) {
                 toResume[i].resume(timeNanos)
@@ -87,12 +92,14 @@
             awaiter = FrameAwaiter(onFrame, co)
             val hadAwaiters = awaiters.isNotEmpty()
             awaiters.add(awaiter)
+            if (!hadAwaiters) hasAwaitersUnlocked.set(1)
             !hadAwaiters
         }
 
         co.invokeOnCancellation {
             synchronized(lock) {
                 awaiters.remove(awaiter)
+                if (awaiters.isEmpty()) hasAwaitersUnlocked.set(0)
             }
         }
 
@@ -116,6 +123,7 @@
                 awaiter.continuation.resumeWithException(cause)
             }
             awaiters.clear()
+            hasAwaitersUnlocked.set(0)
         }
     }
 
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
index 50fa39f..a6e59b3 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
@@ -23,16 +23,19 @@
 import androidx.compose.runtime.mock.expectNoChanges
 import androidx.compose.runtime.snapshots.Snapshot
 import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.test.Ignore
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
 import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.newSingleThreadContext
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -437,6 +440,41 @@
         assertEquals<List<Set<Any>>>(listOf(setOf(countFromEffect)), applications)
     }
 
+    @Ignore // b/329682091
+    @OptIn(DelicateCoroutinesApi::class)
+    @Test // b/329011032
+    fun validatePotentialDeadlock() = compositionTest {
+        var state by mutableIntStateOf(0)
+        compose {
+            repeat(1000) {
+                Text("This is some text: $state")
+            }
+            LaunchedEffect(Unit) {
+                newSingleThreadContext("other thread").use {
+                    while (true) {
+                        withContext(it) {
+                            state++
+                            Snapshot.registerGlobalWriteObserver { }.dispose()
+                        }
+                    }
+                }
+            }
+            LaunchedEffect(Unit) {
+                while (true) {
+                    withFrameNanos {
+                        state++
+                        Snapshot.sendApplyNotifications()
+                    }
+                }
+            }
+        }
+
+        repeat(10) {
+            state++
+            advance(ignorePendingWork = true)
+        }
+    }
+
     @Test
     fun pausingTheFrameClockStopShouldBlockWithFrameNanos() {
         val dispatcher = StandardTestDispatcher()
diff --git a/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt b/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
index c8107cb..7e76cce 100644
--- a/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
+++ b/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
@@ -17,17 +17,27 @@
 package androidx.compose.testutils
 
 import androidx.activity.ComponentActivity
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.test.junit4.AndroidComposeTestRule
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.yield
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -40,7 +50,9 @@
     val composeTestRule = createAndroidComposeRule<ComponentActivity>()
 
     internal fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>
-    .forGivenContent(composable: @Composable () -> Unit): ComposeTestCaseSetup {
+        .forGivenContent(
+        composable: @Composable () -> Unit
+    ): ComposeTestCaseSetup {
         return forGivenTestCase(object : ComposeTestCase {
             @Composable
             override fun Content() {
@@ -184,6 +196,115 @@
         }
     }
 
+    @Test
+    fun countLaunchedCoroutines_noContentLaunches() {
+        composeTestRule.forGivenContent {
+            Box {
+                Text("Hello")
+            }
+        }.performTestWithEventsControl {
+            assertCoroutinesCount(0)
+        }
+    }
+
+    @Test
+    fun countLaunchedCoroutines_modifierLaunches() {
+        val node = object : Modifier.Node() {
+            override fun onAttach() {
+                super.onAttach()
+                coroutineScope.launch { }
+            }
+        }
+        val element = object : ModifierNodeElement<Modifier.Node>() {
+            override fun create(): Modifier.Node = node
+
+            override fun update(node: Modifier.Node) {
+                // no op
+            }
+
+            override fun hashCode(): Int = 0
+
+            override fun equals(other: Any?): Boolean = false
+        }
+        composeTestRule.forGivenContent {
+            Box(Modifier.then(element)) {
+                Text("Hello")
+            }
+        }.performTestWithEventsControl {
+            assertCoroutinesCount(1)
+        }
+    }
+
+    @Test
+    fun countLaunchedCoroutines_launchedEffect() {
+        composeTestRule.forGivenContent {
+            LaunchedEffect(Unit) {
+                launch { }
+            }
+        }.performTestWithEventsControl {
+            assertCoroutinesCount(2)
+        }
+    }
+
+    @Test
+    fun countLaunchedCoroutines_scopeLaunches_lazy() {
+        composeTestRule.forGivenContent {
+            val scope = rememberCoroutineScope()
+            Box(Modifier.clickable {
+                scope.launch { }
+            }) {
+                Text("Hello")
+            }
+        }.performTestWithEventsControl {
+            assertCoroutinesCount(0)
+        }
+    }
+
+    @Test
+    fun countLaunchedCoroutines_suspend() {
+        composeTestRule.forGivenContent {
+            LaunchedEffect(Unit) {
+                suspendCancellableCoroutine {}
+            }
+
+            LaunchedEffect(Unit) {
+                suspendCoroutine {}
+            }
+        }.performTestWithEventsControl {
+            assertCoroutinesCount(2)
+        }
+    }
+
+    @Test
+    fun countLaunchedCoroutines_delay() {
+        composeTestRule.forGivenContent {
+            LaunchedEffect(Unit) {
+                delay(1_000L)
+            }
+
+            LaunchedEffect(Unit) {
+                launch { }
+            }
+        }.performTestWithEventsControl {
+            assertCoroutinesCount(3)
+        }
+    }
+
+    @Test
+    fun countLaunchedCoroutines_yield() {
+        composeTestRule.forGivenContent {
+            LaunchedEffect(Unit) {
+                yield()
+            }
+
+            LaunchedEffect(Unit) {
+                launch { }
+            }
+        }.performTestWithEventsControl {
+            assertCoroutinesCount(3)
+        }
+    }
+
     private inline fun <reified T : Throwable> assertFailsWith(
         expectedErrorMessage: String? = null,
         block: () -> Any
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt
index 21fcab5..3bb41e2 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt
@@ -35,8 +35,12 @@
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.TestMonotonicFrameClock
 import androidx.compose.ui.test.frameDelayMillis
+import androidx.compose.ui.test.internal.DelayPropagatingContinuationInterceptorWrapper
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.ContinuationInterceptor
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
@@ -93,9 +97,12 @@
         CoroutineScope(testCoroutineDispatcher + testCoroutineDispatcher.scheduler)
     )
 
+    private val continuationCountInterceptor =
+        ContinuationCountInterceptor(frameClock.continuationInterceptor)
+
     @OptIn(ExperimentalTestApi::class)
     private val recomposerApplyCoroutineScope = CoroutineScope(
-        frameClock + frameClock.continuationInterceptor + Job()
+        continuationCountInterceptor + frameClock + Job()
     )
     private val recomposer: Recomposer = Recomposer(recomposerApplyCoroutineScope.coroutineContext)
         .also { recomposerApplyCoroutineScope.launch { it.runRecomposeAndApplyChanges() } }
@@ -131,6 +138,7 @@
             "Need to call onPreEmitContent before emitContent!"
         }
 
+        continuationCountInterceptor.reset()
         activity.setContent(recomposer) { testCase!!.Content() }
         view = owner!!.view
         Snapshot.notifyObjectsInitialized()
@@ -302,6 +310,10 @@
     override fun getTestCase(): T {
         return testCase!!
     }
+
+    override fun getCoroutineLaunchedCount(): Int {
+        return continuationCountInterceptor.continuationCount - InternallyLaunchedCoroutines
+    }
 }
 
 private enum class SimulationState {
@@ -404,3 +416,25 @@
         return Bitmap.createBitmap(picture)
     }
 }
+
+@OptIn(InternalTestApi::class)
+private class ContinuationCountInterceptor(private val parentInterceptor: ContinuationInterceptor) :
+    DelayPropagatingContinuationInterceptorWrapper(parentInterceptor) {
+    var continuationCount = 0
+        private set
+
+    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
+        continuationCount++
+        return parentInterceptor.interceptContinuation(continuation)
+    }
+
+    override fun releaseInterceptedContinuation(continuation: Continuation<*>) {
+        parentInterceptor.releaseInterceptedContinuation(continuation)
+    }
+
+    fun reset() {
+        continuationCount = 0
+    }
+}
+
+private const val InternallyLaunchedCoroutines = 4
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeExecutionControl.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeExecutionControl.kt
index 7138435..cce6330 100644
--- a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeExecutionControl.kt
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeExecutionControl.kt
@@ -112,6 +112,11 @@
      * This API may be removed in the future.
      */
     fun getHostView(): NativeView
+
+    /**
+     * A count on launched jobs in the composition.
+     */
+    fun getCoroutineLaunchedCount(): Int
 }
 
 /**
@@ -254,3 +259,13 @@
             "frames."
     )
 }
+
+@UiThread
+fun ComposeExecutionControl.assertCoroutinesCount(expectedCount: Int) {
+    val actual = getCoroutineLaunchedCount()
+    if (getCoroutineLaunchedCount() != expectedCount) {
+        throw AssertionError(
+            "Coroutines launched is $actual when $expectedCount were expected."
+        )
+    }
+}
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/Expect.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/Expect.kt
index 8c2e559..b2f3744 100644
--- a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/Expect.kt
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/Expect.kt
@@ -26,7 +26,7 @@
  * [expectedMessage] is a regex with just the option [DOT_MATCHES_ALL] enabled.
  */
 fun expectAssertionError(
-    expectError: Boolean,
+    expectError: Boolean = true,
     expectedMessage: String = ".*",
     block: () -> Unit
 ) {
diff --git a/compose/ui/ui-graphics/api/res-current.txt b/compose/ui/ui-graphics/api/res-current.txt
index e69de29..4553236 100644
--- a/compose/ui/ui-graphics/api/res-current.txt
+++ b/compose/ui/ui-graphics/api/res-current.txt
@@ -0,0 +1 @@
+id hide_graphics_layer_in_inspector_tag
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt
index 23185dd..3aed02d 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt
@@ -52,7 +52,7 @@
         clipToPadding = false
 
         // Hide this view and its children in tools:
-        setTag(R.id.hide_in_inspector_tag, true)
+        setTag(R.id.hide_graphics_layer_in_inspector_tag, true)
     }
 
     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
diff --git a/compose/ui/ui-graphics/src/androidMain/res/values/ids.xml b/compose/ui/ui-graphics/src/androidMain/res/values/ids.xml
index 989568a..453cbdb 100644
--- a/compose/ui/ui-graphics/src/androidMain/res/values/ids.xml
+++ b/compose/ui/ui-graphics/src/androidMain/res/values/ids.xml
@@ -16,5 +16,5 @@
   -->
 
 <resources>
-    <item name="hide_in_inspector_tag" type="id" />
+    <item name="hide_graphics_layer_in_inspector_tag" type="id" />
 </resources>
diff --git a/compose/ui/ui-graphics/src/androidMain/res/values/public.xml b/compose/ui/ui-graphics/src/androidMain/res/values/public.xml
new file mode 100644
index 0000000..1a24359
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/res/values/public.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <public name="hide_graphics_layer_in_inspector_tag" type="id"/>
+</resources>
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/AndroidComposeViewWrapper.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/AndroidComposeViewWrapper.kt
index 0140b6d..a1a2615 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/AndroidComposeViewWrapper.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/AndroidComposeViewWrapper.kt
@@ -21,7 +21,7 @@
 import android.view.ViewGroup
 import androidx.collection.LongList
 import androidx.collection.mutableLongListOf
-import androidx.compose.ui.graphics.R
+import androidx.compose.ui.R
 import androidx.compose.ui.inspection.framework.ancestors
 import androidx.compose.ui.inspection.framework.getChildren
 import androidx.compose.ui.inspection.framework.isAndroidComposeView
@@ -113,10 +113,14 @@
     private fun createViewsToSkip(viewGroup: ViewGroup): LongList {
         val result = mutableLongListOf()
         viewGroup.getChildren().forEach { view ->
-            if (view.getTag(R.id.hide_in_inspector_tag) != null) {
+            if (view.hasHideFromInspectionTag()) {
                 result.add(view.uniqueDrawingId)
             }
         }
         return result
     }
+
+    private fun View.hasHideFromInspectionTag(): Boolean =
+        getTag(R.id.hide_in_inspector_tag) != null ||
+            getTag(androidx.compose.ui.graphics.R.id.hide_graphics_layer_in_inspector_tag) != null
 }
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
index 22f8500..d5fe7f0 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
@@ -20,7 +20,6 @@
 
 import androidx.compose.lint.Names
 import androidx.compose.lint.inheritsFrom
-import androidx.compose.lint.toKmFunction
 import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ModifierFactoryReturnType
 import com.android.tools.lint.client.api.UElementHandler
 import com.android.tools.lint.detector.api.Category
@@ -34,9 +33,12 @@
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiType
-import com.intellij.psi.impl.compiled.ClsMethodImpl
 import java.util.EnumSet
-import kotlinx.metadata.KmClassifier
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.calls.KtCall
+import org.jetbrains.kotlin.analysis.api.calls.KtCallableMemberCall
+import org.jetbrains.kotlin.analysis.api.calls.singleCallOrNull
+import org.jetbrains.kotlin.psi.KtCallExpression
 import org.jetbrains.kotlin.psi.KtCallableDeclaration
 import org.jetbrains.kotlin.psi.KtDeclarationWithBody
 import org.jetbrains.kotlin.psi.KtFunction
@@ -48,9 +50,6 @@
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UMethod
 import org.jetbrains.uast.UThisExpression
-import org.jetbrains.uast.UTypeReferenceExpression
-import org.jetbrains.uast.getContainingUClass
-import org.jetbrains.uast.resolveToUElement
 import org.jetbrains.uast.toUElement
 import org.jetbrains.uast.tryResolve
 import org.jetbrains.uast.visitor.AbstractUastVisitor
@@ -237,33 +236,17 @@
         override fun visitCallExpression(node: UCallExpression): Boolean {
             // We account for a receiver of `this` in `visitThisExpression`
             if (node.receiver == null) {
-                val declaration = node.resolveToUElement()
-                // If the declaration is a member of `Modifier` (such as `then`)
-                if (declaration?.getContainingUClass()
-                    ?.qualifiedName == Names.Ui.Modifier.javaFqn
-                ) {
-                    isReceiverReferenced = true
-                    // Otherwise if the declaration is an extension of `Modifier`
-                } else {
-                    // Whether the declaration itself has a Modifier receiver - UAST might think the
-                    // receiver on the node is different if it is inside another scope.
-                    val hasModifierReceiver = when (val source = declaration?.sourcePsi) {
-                        // Parsing a method defined in a class file
-                        is ClsMethodImpl -> {
-                            val receiverClassifier = source.toKmFunction()
-                                ?.receiverParameterType?.classifier
-                            receiverClassifier == KmClassifier.Class(Names.Ui.Modifier.kmClassName)
-                        }
-                        // Parsing a method defined in Kotlin source
-                        is KtFunction -> {
-                            val receiver = source.receiverTypeReference
-                            (receiver.toUElement() as? UTypeReferenceExpression)
-                                ?.getQualifiedName() == Names.Ui.Modifier.javaFqn
-                        }
-                        else -> false
-                    }
-                    if (hasModifierReceiver) {
+                val ktCallExpression = node.sourcePsi as? KtCallExpression
+                    ?: return isReceiverReferenced
+                analyze(ktCallExpression) {
+                    val ktCall = ktCallExpression.resolveCall()?.singleCallOrNull<KtCall>()
+                    val callee = (ktCall as? KtCallableMemberCall<*, *>)?.partiallyAppliedSymbol
+                    val receiver = callee?.extensionReceiver ?: callee?.dispatchReceiver
+                    val receiverClass = receiver?.type?.expandedClassSymbol?.classIdIfNonLocal
+                    if (receiverClass?.asFqNameString() == Names.Ui.Modifier.javaFqn) {
                         isReceiverReferenced = true
+                        // no further tree traversal, since we found receiver usage.
+                        return true
                     }
                 }
             }
@@ -277,7 +260,8 @@
          */
         override fun visitThisExpression(node: UThisExpression): Boolean {
             isReceiverReferenced = true
-            return isReceiverReferenced
+            // no further tree traversal, since we found receiver usage.
+            return true
         }
     })
     if (!isReceiverReferenced) {
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
index 3c53513..80b3d8d 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
@@ -19,7 +19,6 @@
 package androidx.compose.ui.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
@@ -40,73 +39,6 @@
     override fun getIssues(): MutableList<Issue> =
         mutableListOf(ComposedModifierDetector.UnnecessaryComposedModifier)
 
-    /**
-     * Simplified Modifier.composed stub
-     */
-    private val composedStub = bytecodeStub(
-        filename = "ComposedModifier.kt",
-        filepath = "androidx/compose/ui",
-        checksum = 0xc6ba0d09,
-        """
-            package androidx.compose.ui
-
-            import androidx.compose.runtime.Composable
-
-            fun Modifier.composed(
-                inspectorInfo: () -> Unit = {},
-                factory: @Composable Modifier.() -> Modifier
-            ): Modifier = this
-        """,
-"""
-        META-INF/main.kotlin_module:
-        H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgUuOSSMxLKcrPTKnQS87PLcgvTtUr
-        Ks0rycxNFeIKSs1NzU1KLfIu4dLkEsZQV5opJOQMYaf45qdkpmWClfJxsZSk
-        FpcIsYUASe8SJQYtBgBxwST5ewAAAA==
-        """,
-        """
-        androidx/compose/ui/ComposedModifierKt$composed$1.class:
-        H4sIAAAAAAAA/5VU6U4TURT+7rR0GYoti7KIO2ILyrTg3oaENBAnFEwEmxh+
-        3XYGuHR6x8xMG/zHK/gKPoFoIokmhvjThzKeO20NbqCTzJ2Tc77v7He+fvv4
-        GcBdPGYocGl5rrD2jbrbfOn6ttESRrkjWmuuJbaF7a0GU12rNVWIgzGsVhpu
-        4Ahp7LWbhpCB7UnuGBXerFm8eNK23ZL1QLjSN1a6Ur7Usz+XIiguFhkm/u4s
-        jijD5dMdxhFjiJUEuVtkiGRzVYZo1sxVU0hA19GHflIEu8JnWKj8d8GUYEzI
-        ttuwGUayucoeb3PD4XLHeFrbs+tBMYU0kjo0DDL0n6gtjmGGhLm+sbm0Xl5m
-        GPip8BTO40ISIxglUKnuhOmrjENXE8p8LknSJMNgj7hmB9ziAaeUtGY7QkNk
-        6kiqAwysoYQIGfeFkvIkWQWGyeODhH58oGsZjT6Z44MJLc+e6F/exLSEpjDz
-        lHiJS1e+arotn5pIzqb/rVFx3GbI/OiWZW/zlhMwvM7+sdM94llLcoa9UDR/
-        n0Pu9IgpzMFgGP61hrkGpRstuxbNd6ji1rlT5Z7gNcfeVAdDuiKkvd5q1myv
-        q0mZUtpe2eG+b9NSpZdl3XF9IXdoQLuuxZDcEDuSBy2PwPqG2/Lq9opQzPFn
-        LRmIpl0VviBXS1K6AQ9rQ56G3UeNpwuGcTV9mmCUXtoI0syTNEUImg1iM5Ej
-        pA7DmS/QmepoMRByBtU6dhmzIYZeBdboyiuYUsRPEFmHmFkiYqZLnFfrpILP
-        fMDQO4y9PYWf6AZOUNq9wKOEVk//J2gvjnDxPS4dhoo+3KNTJ1gHMIb7YZ13
-        qP4HYZAIHobfAh6Ffym6/8S6soWIiasmrpm4jhsmNeOmiWnc2gLzkUWO7D5m
-        fMz6SH8Hu1pp5uIEAAA=
-        """,
-        """
-        androidx/compose/ui/ComposedModifierKt.class:
-        H4sIAAAAAAAA/7VUUU8bRxD+9mzss2OIsUlCHEJo4yRgSM4maZvWhAShIJ1q
-        3CqmvPC0+NZk8XkP3Z0ReYn4C33sa39B1aeoDxXqY6X+paqz53MggHClqjrd
-        7MzOzLffzs7un3//9juAZ1hjeMiV43vSObLaXu/AC4TVl9b6QHU2PUd2pPC/
-        DdNgDPl9fsgtl6s967vdfdGm2QSDGSc6DO/nG5fBDWHqja4XulJZ+4c9q9NX
-        7VB6KrA2Yq06wl+rL1wNz/DXfyOwMvT/oGRYXx3FZ+Xx1astXu1eHb2f+w3P
-        37P2Rbjrc0lLc6W8kA9oNL2w2XddikqthG9lsGoiwzB7hrJUofAVdy1bhT6l
-        y3aQxjWGG+23ot2N87/nPu8JCmR4NN84f8L1MzMtDbJXX9jOYRwTWeRwnWGc
-        cA8o0PNt1fFMTDKkO1zb70wUGSbKmlv5tEdmR+15blSXjAypUUh+uGLZER3e
-        d0OGH//n7rQvVm/kAdf+3fX7WL9yLY07dOfsZmtrrbn+muHppUtcCVHP4S5m
-        M5jBvU8b5pJdp/FZDmNIZWHgPsPksAibIuQODzntwegdJug5YVpktAAD62rF
-        IOeR1FqVNKfGwE+OZ7Mnx1lj2hgORv5UHfyl5/mT45JRZRX6lydMiiiZhWTB
-        qCarieWZ/FhpOrLYQFZTf/ycMsx0JE290DJDYUj0bN/gknndLA8u1tDvq1D2
-        RFxIvuuKuu7dOPn1USjoPnlqiLL17kAHFM/X/UmXGi+57jmC4XpDKtHs93aF
-        v6UBNRmvzd1t7kttx5OZltxTPOz7pN95M6Bhq0MZSHKvnT4ADOXz3o93+ZOw
-        8VbI291NfhAvkLOVEv66y4NAkDvb8vp+W2xI7bsdQ25fWA416oCkPl0ab+uW
-        IOsrst6QrY94qlLIfkB+sVAguVSYIln5JYp+TjKla48MviZ9bhCPG7gZ4U1h
-        ErfIr7UipinjmygvjXqcadK4Qn8xERtnZD5DdEqkazIvCHpMA91Nvv8J2V8x
-        d4LPG5XFpQ8oD8i8IEko4xGriYhJir40vWkpslbJzhLYTMRsGi+jpC/xisYN
-        mn9A8A93kLDxyMa8jQVUbCxiycZjPNkBC2ChuoNMgLEANwNMUuECLAcoBnga
-        4FmAL/4BKp/c6X8HAAA=
-        """
-    )
-
     @Test
     fun noComposableCalls() {
         lint().files(
@@ -135,7 +67,7 @@
                 fun Modifier.test4(): Modifier = composed({}, { this@test4})
             """
             ),
-            composedStub,
+            UiStubs.composed,
             Stubs.Composable,
             Stubs.Modifier
         )
@@ -224,7 +156,7 @@
                 }
             """
             ),
-            composedStub,
+            UiStubs.composed,
             Stubs.Composable,
             Stubs.Modifier,
             Stubs.Remember
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
index aba348a..9b1c94b 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
@@ -856,5 +856,28 @@
             .run()
             .expectClean()
     }
+
+    @Test
+    fun composedNoErrors() {
+        // Regression test from b/328119668
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.ui.foo
+
+                import androidx.compose.ui.Modifier
+                import androidx.compose.ui.composed
+
+                fun Modifier.bar(): Modifier = composed {
+                    object : Modifier {}
+                }
+            """
+            ),
+            Stubs.Modifier,
+            UiStubs.composed,
+        )
+            .run()
+            .expectClean()
+    }
 }
 /* ktlint-enable max-line-length */
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
index 4618e50..d233509 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
@@ -239,4 +239,71 @@
         wwpmKpjFHH3iUQXzKO6DaTzGk30kNByNBY2URv43qNs4vuQDAAA=
         """
     )
+
+    /**
+     * Simplified Modifier.composed stub
+     */
+    val composed = bytecodeStub(
+        filename = "ComposedModifier.kt",
+        filepath = "androidx/compose/ui",
+        checksum = 0xc6ba0d09,
+        """
+            package androidx.compose.ui
+
+            import androidx.compose.runtime.Composable
+
+            fun Modifier.composed(
+                inspectorInfo: () -> Unit = {},
+                factory: @Composable Modifier.() -> Modifier
+            ): Modifier = this
+        """,
+"""
+        META-INF/main.kotlin_module:
+        H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgUuOSSMxLKcrPTKnQS87PLcgvTtUr
+        Ks0rycxNFeIKSs1NzU1KLfIu4dLkEsZQV5opJOQMYaf45qdkpmWClfJxsZSk
+        FpcIsYUASe8SJQYtBgBxwST5ewAAAA==
+        """,
+        """
+        androidx/compose/ui/ComposedModifierKt$composed$1.class:
+        H4sIAAAAAAAA/5VU6U4TURT+7rR0GYoti7KIO2ILyrTg3oaENBAnFEwEmxh+
+        3XYGuHR6x8xMG/zHK/gKPoFoIokmhvjThzKeO20NbqCTzJ2Tc77v7He+fvv4
+        GcBdPGYocGl5rrD2jbrbfOn6ttESRrkjWmuuJbaF7a0GU12rNVWIgzGsVhpu
+        4Ahp7LWbhpCB7UnuGBXerFm8eNK23ZL1QLjSN1a6Ur7Usz+XIiguFhkm/u4s
+        jijD5dMdxhFjiJUEuVtkiGRzVYZo1sxVU0hA19GHflIEu8JnWKj8d8GUYEzI
+        ttuwGUayucoeb3PD4XLHeFrbs+tBMYU0kjo0DDL0n6gtjmGGhLm+sbm0Xl5m
+        GPip8BTO40ISIxglUKnuhOmrjENXE8p8LknSJMNgj7hmB9ziAaeUtGY7QkNk
+        6kiqAwysoYQIGfeFkvIkWQWGyeODhH58oGsZjT6Z44MJLc+e6F/exLSEpjDz
+        lHiJS1e+arotn5pIzqb/rVFx3GbI/OiWZW/zlhMwvM7+sdM94llLcoa9UDR/
+        n0Pu9IgpzMFgGP61hrkGpRstuxbNd6ji1rlT5Z7gNcfeVAdDuiKkvd5q1myv
+        q0mZUtpe2eG+b9NSpZdl3XF9IXdoQLuuxZDcEDuSBy2PwPqG2/Lq9opQzPFn
+        LRmIpl0VviBXS1K6AQ9rQ56G3UeNpwuGcTV9mmCUXtoI0syTNEUImg1iM5Ej
+        pA7DmS/QmepoMRByBtU6dhmzIYZeBdboyiuYUsRPEFmHmFkiYqZLnFfrpILP
+        fMDQO4y9PYWf6AZOUNq9wKOEVk//J2gvjnDxPS4dhoo+3KNTJ1gHMIb7YZ13
+        qP4HYZAIHobfAh6Ffym6/8S6soWIiasmrpm4jhsmNeOmiWnc2gLzkUWO7D5m
+        fMz6SH8Hu1pp5uIEAAA=
+        """,
+        """
+        androidx/compose/ui/ComposedModifierKt.class:
+        H4sIAAAAAAAA/7VUUU8bRxD+9mzss2OIsUlCHEJo4yRgSM4maZvWhAShIJ1q
+        3CqmvPC0+NZk8XkP3Z0ReYn4C33sa39B1aeoDxXqY6X+paqz53MggHClqjrd
+        7MzOzLffzs7un3//9juAZ1hjeMiV43vSObLaXu/AC4TVl9b6QHU2PUd2pPC/
+        DdNgDPl9fsgtl6s967vdfdGm2QSDGSc6DO/nG5fBDWHqja4XulJZ+4c9q9NX
+        7VB6KrA2Yq06wl+rL1wNz/DXfyOwMvT/oGRYXx3FZ+Xx1astXu1eHb2f+w3P
+        37P2Rbjrc0lLc6W8kA9oNL2w2XddikqthG9lsGoiwzB7hrJUofAVdy1bhT6l
+        y3aQxjWGG+23ot2N87/nPu8JCmR4NN84f8L1MzMtDbJXX9jOYRwTWeRwnWGc
+        cA8o0PNt1fFMTDKkO1zb70wUGSbKmlv5tEdmR+15blSXjAypUUh+uGLZER3e
+        d0OGH//n7rQvVm/kAdf+3fX7WL9yLY07dOfsZmtrrbn+muHppUtcCVHP4S5m
+        M5jBvU8b5pJdp/FZDmNIZWHgPsPksAibIuQODzntwegdJug5YVpktAAD62rF
+        IOeR1FqVNKfGwE+OZ7Mnx1lj2hgORv5UHfyl5/mT45JRZRX6lydMiiiZhWTB
+        qCarieWZ/FhpOrLYQFZTf/ycMsx0JE290DJDYUj0bN/gknndLA8u1tDvq1D2
+        RFxIvuuKuu7dOPn1USjoPnlqiLL17kAHFM/X/UmXGi+57jmC4XpDKtHs93aF
+        v6UBNRmvzd1t7kttx5OZltxTPOz7pN95M6Bhq0MZSHKvnT4ADOXz3o93+ZOw
+        8VbI291NfhAvkLOVEv66y4NAkDvb8vp+W2xI7bsdQ25fWA416oCkPl0ab+uW
+        IOsrst6QrY94qlLIfkB+sVAguVSYIln5JYp+TjKla48MviZ9bhCPG7gZ4U1h
+        ErfIr7UipinjmygvjXqcadK4Qn8xERtnZD5DdEqkazIvCHpMA91Nvv8J2V8x
+        d4LPG5XFpQ8oD8i8IEko4xGriYhJir40vWkpslbJzhLYTMRsGi+jpC/xisYN
+        mn9A8A93kLDxyMa8jQVUbCxiycZjPNkBC2ChuoNMgLEANwNMUuECLAcoBnga
+        4FmAL/4BKp/c6X8HAAA=
+        """
+    )
 }
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
index 4b564b4..b18f876 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
@@ -66,7 +66,7 @@
         rule.onNodeWithText("Hello")
             .assertExists()
 
-        expectAssertionError(true) {
+        expectAssertionError {
             rule.onNodeWithText("Hello")
                 .assertDoesNotExist()
         }
@@ -83,12 +83,12 @@
         cachedResult
             .assertDoesNotExist()
 
-        expectAssertionError(true) {
+        expectAssertionError {
             rule.onNodeWithText("Hello")
                 .assertExists()
         }
 
-        expectAssertionError(true) {
+        expectAssertionError {
             cachedResult.assertExists()
         }
 
@@ -99,7 +99,7 @@
         rule.onNodeWithText("Hello")
             .assertExists()
 
-        expectAssertionError(true) {
+        expectAssertionError {
             rule.onNodeWithText("Hello")
                 .assertDoesNotExist()
         }
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 0cad628..af80153 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -117,6 +117,10 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTextApi {
   }
 
+  public final class Html_androidKt {
+    method public static androidx.compose.ui.text.AnnotatedString parseAsHtml(String);
+  }
+
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
   }
 
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 8fd548b..675ad44 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -117,6 +117,10 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTextApi {
   }
 
+  public final class Html_androidKt {
+    method public static androidx.compose.ui.text.AnnotatedString parseAsHtml(String);
+  }
+
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
   }
 
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringFromHtmlSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringFromHtmlSamples.kt
new file mode 100644
index 0000000..c068272
--- /dev/null
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringFromHtmlSamples.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.compose.ui.text.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.parseAsHtml
+
+@Composable
+@Sampled
+fun AnnotatedStringFromHtml() {
+    // First, download a string as a plain text using one of the resources' methods. At this stage
+    // you will be handling plurals and formatted strings in needed. Moreover, the string will be
+    // resolved with respect to the current locale and available translations.
+    val string = stringResource(id = R.string.example)
+
+    // Next, convert a string marked with HTML tags into AnnotatedString to be displayed by Text
+    val styledAnnotatedString = string.parseAsHtml()
+
+    BasicText(styledAnnotatedString)
+}
diff --git a/compose/ui/ui-text/samples/src/main/res/values/styled-string-for-sample.xml b/compose/ui/ui-text/samples/src/main/res/values/styled-string-for-sample.xml
new file mode 100644
index 0000000..5c0272e
--- /dev/null
+++ b/compose/ui/ui-text/samples/src/main/res/values/styled-string-for-sample.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <string name="example" translatable="false">
+        &lt;b>bold&lt;/b>
+        &lt;i>italic&lt;/i>
+        &lt;big>big&lt;/big>
+        &lt;small>small&lt;/small>
+        &lt;font face="monospace">monospace&lt;/font>
+        &lt;font face="serif">serif&lt;/font>
+        &lt;font face="sans_serif">sans_serif&lt;/font>
+        &lt;font face="cursive">cursive&lt;/font>
+        &lt;font color="#00ff00">green&lt;/font>
+        &lt;tt>monospace&lt;/tt>
+        &lt;sup>superscript&lt;/sup>
+        &lt;strike>strikethrough&lt;/strike>
+        &lt;sub>subscript&lt;/sub>
+        &lt;u>underline&lt;/u>
+        &lt;span style="background-color:#ff0000">span&lt;/span>
+        &lt;p dir="rtl">right to left&lt;/p>
+        &lt;p dir="ltr">left to right&lt;/p>
+        I am &lt;div>div&lt;/div> element.&lt;br>
+        &lt;a href="https://developer.android.com">Link&lt;/a>
+    </string>
+</resources>
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/AndroidManifest.xml b/compose/ui/ui-text/src/androidInstrumentedTest/AndroidManifest.xml
new file mode 100644
index 0000000..934bf4b
--- /dev/null
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?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">
+    <application>
+        <activity android:name="androidx.activity.ComponentActivity" />
+    </application>
+</manifest>
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AnnotatedStringFromHtmlTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AnnotatedStringFromHtmlTest.kt
new file mode 100644
index 0000000..c80d23df
--- /dev/null
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AnnotatedStringFromHtmlTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.em
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AnnotatedStringFromHtmlTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    // pre-N block-level elements were separated with two new lines
+    @SdkSuppress(minSdkVersion = 24)
+    fun buildAnnotatedString_fromHtml() {
+        rule.setContent {
+            val expected = buildAnnotatedString {
+                fun add(block: () -> Unit) {
+                    block()
+                    append("a")
+                    pop()
+                    append(" ")
+                }
+                fun addStyle(style: SpanStyle) {
+                    add { pushStyle(style) }
+                }
+
+                add { pushLink(LinkAnnotation.Url("https://example.com")) }
+                add { pushStringAnnotation("foo", "Bar") }
+                addStyle(SpanStyle(fontWeight = FontWeight.Bold))
+                addStyle(SpanStyle(fontSize = 1.25.em))
+                append("\na\n") // <div>
+                addStyle(SpanStyle(fontFamily = FontFamily.Serif))
+                addStyle(SpanStyle(color = Color.Green))
+                addStyle(SpanStyle(fontStyle = FontStyle.Italic))
+                append("\na\n") // <p>
+                addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough))
+                addStyle(SpanStyle(fontSize = 0.8.em))
+                addStyle(SpanStyle(background = Color.Red))
+                addStyle(SpanStyle(baselineShift = BaselineShift.Subscript))
+                addStyle(SpanStyle(baselineShift = BaselineShift.Superscript))
+                addStyle(SpanStyle(fontFamily = FontFamily.Monospace))
+                addStyle(SpanStyle(textDecoration = TextDecoration.Underline))
+            }
+
+            val actual = stringResource(androidx.compose.ui.text.test.R.string.html).parseAsHtml()
+
+            assertThat(actual.text).isEqualTo(expected.text)
+            assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles).inOrder()
+            assertThat(actual.paragraphStyles)
+                .containsExactlyElementsIn(expected.paragraphStyles)
+                .inOrder()
+            assertThat(actual.getStringAnnotations(0, actual.length))
+                .containsExactlyElementsIn(expected.getStringAnnotations(0, expected.length))
+                .inOrder()
+            assertThat(actual.getLinkAnnotations(0, actual.length))
+                .containsExactlyElementsIn(expected.getLinkAnnotations(0, expected.length))
+                .inOrder()
+        }
+    }
+
+    @Test
+    fun formattedString_withStyling() {
+        rule.setContent {
+            val actual = stringResource(
+                androidx.compose.ui.text.test.R.string.formatting,
+                "computer"
+            ).parseAsHtml()
+            assertThat(actual.text).isEqualTo("Hello, computer!")
+            assertThat(actual.spanStyles).containsExactly(
+                AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold), 7, 15)
+            )
+        }
+    }
+
+    @Test
+    fun annotationTag_withNoText_noStringAnnotation() {
+        expectExactly(
+            "a<annotation key1=value1></annotation>",
+            "a"
+        )
+    }
+
+    @Test
+    fun annotationTag_withNoAttributes_noStringAnnotation() {
+        expectExactly(
+            "<annotation>a</annotation>",
+            "a"
+        )
+    }
+
+    @Test
+    fun annotationTag_withOneAttribute_oneStringAnnotation() {
+        expectExactly(
+            "<annotation key1=value1>a</annotation>",
+            "a",
+            AnnotatedString.Range("value1", 0, 1, "key1")
+        )
+    }
+
+    @Test
+    fun annotationTag_withMultipleAttributes_multipleStringAnnotations() {
+        expectExactly(
+            "<annotation key1=\"value1\" key2=value2 keyThree=\"valueThree\">a</annotation>",
+            "a",
+            AnnotatedString.Range("value1", 0, 1, "key1"),
+            AnnotatedString.Range("value2", 0, 1, "key2"),
+            AnnotatedString.Range("valueThree", 0, 1, "keythree")
+        )
+    }
+
+    @Test
+    fun annotationTag_withMultipleAnnotations_multipleStringAnnotations() {
+        expectExactly(
+            "<annotation key1=\"value1\">a</annotation>a<annotation key2=\"value2\">a</annotation>",
+            "aaa",
+            AnnotatedString.Range("value1", 0, 1, "key1"),
+            AnnotatedString.Range("value2", 2, 3, "key2")
+        )
+    }
+
+    @Test
+    fun annotationTag_withOtherTag() {
+        expectExactly(
+            "<annotation key1=\"value1\">a</annotation><b>a</b>",
+            "aa",
+            AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold), 1, 2),
+            AnnotatedString.Range("value1", 0, 1, "key1"),
+        )
+    }
+
+    @Test
+    fun annotationTag_wrappedByOtherTag() {
+        expectExactly(
+            "<b><annotation key1=\"value1\">a</annotation></b>",
+            "a",
+            AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold), 0, 1),
+            AnnotatedString.Range("value1", 0, 1, "key1")
+        )
+    }
+
+    private fun expectExactly(
+        actualTaggedString: String,
+        expectedString: String,
+        vararg expectedStringAnnotations: AnnotatedString.Range<Any>
+    ) {
+        rule.setContent {
+            val actual = actualTaggedString.parseAsHtml()
+            assertThat(actual.text).isEqualTo(expectedString)
+            assertThat(actual.spanStyles).containsExactlyElementsIn(
+                expectedStringAnnotations.filter {
+                    it.item is SpanStyle
+                }
+            )
+            assertThat(actual.getStringAnnotations(0, actual.length))
+                .containsExactlyElementsIn(expectedStringAnnotations.filter {
+                    it.item is String
+                })
+        }
+    }
+}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/res/values/styled-string-for-test.xml b/compose/ui/ui-text/src/androidInstrumentedTest/res/values/styled-string-for-test.xml
new file mode 100644
index 0000000..c20f742
--- /dev/null
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/res/values/styled-string-for-test.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <string name="html" translatable="false">
+        &lt;a href="https://example.com">a&lt;/a>
+        &lt;annotation Foo=Bar>a&lt;/annotation>
+        &lt;b>a&lt;/b>
+        &lt;big>a&lt;/big>
+        &lt;div>a&lt;/div>
+        &lt;font face="serif">a&lt;/font>
+        &lt;font color="#00ff00">a&lt;/font>
+        &lt;i>a&lt;/i>
+        &lt;p>a&lt;/p>
+        &lt;s>a&lt;/s>
+        &lt;small>a&lt;/small>
+        &lt;span style="background-color:red">a&lt;/span>
+        &lt;sub>a&lt;/sub>
+        &lt;sup>a&lt;/sup>
+        &lt;tt>a&lt;/tt>
+        &lt;u>a&lt;/u>
+    </string>
+    <string name="formatting">Hello, &lt;b>%s&lt;/b>!</string>
+</resources>
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
index 68fbac8..5b73220 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
@@ -402,7 +402,8 @@
         )
     }
 
-    private val wordIterator: WordIterator = layout.wordIterator
+    private val wordIterator: WordIterator
+        get() = layout.wordIterator
 
     override fun getWordBoundary(offset: Int): TextRange {
         val wordIterator = layout.wordIterator
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt
new file mode 100644
index 0000000..79ad654
--- /dev/null
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text
+
+import android.graphics.Typeface
+import android.os.Build
+import android.text.Editable
+import android.text.Html.TagHandler
+import android.text.Layout
+import android.text.Spanned
+import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+import android.text.Spanned.SPAN_MARK_MARK
+import android.text.style.AbsoluteSizeSpan
+import android.text.style.AlignmentSpan
+import android.text.style.BackgroundColorSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.SubscriptSpan
+import android.text.style.SuperscriptSpan
+import android.text.style.TypefaceSpan
+import android.text.style.URLSpan
+import android.text.style.UnderlineSpan
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.util.fastForEach
+import androidx.core.text.HtmlCompat
+import org.xml.sax.Attributes
+import org.xml.sax.ContentHandler
+import org.xml.sax.XMLReader
+
+actual fun String.parseAsHtml(): AnnotatedString {
+    // Check ContentHandlerReplacementTag kdoc for more details
+    val stringToParse = "<$ContentHandlerReplacementTag />$this"
+    val spanned = HtmlCompat.fromHtml(
+        stringToParse,
+        HtmlCompat.FROM_HTML_MODE_COMPACT,
+        null,
+        TagHandler
+    )
+    return spanned.toAnnotatedString()
+}
+
+private fun Spanned.toAnnotatedString(): AnnotatedString {
+    return AnnotatedString.Builder(capacity = length)
+        .append(this)
+        .also { it.addSpans(this) }
+        .toAnnotatedString()
+}
+
+private fun AnnotatedString.Builder.addSpans(spanned: Spanned) {
+    spanned.getSpans(0, length, Any::class.java).forEach { span ->
+        val range = TextRange(spanned.getSpanStart(span), spanned.getSpanEnd(span))
+        addSpan(span, range.start, range.end)
+    }
+}
+
+private fun AnnotatedString.Builder.addSpan(span: Any, start: Int, end: Int) {
+    when (span) {
+        is AbsoluteSizeSpan -> {
+            // TODO(soboleva) need density object or make dip/px new units in TextUnit
+        }
+        is AlignmentSpan -> {
+            addStyle(span.toParagraphStyle(), start, end)
+        }
+        is AnnotationSpan -> {
+            addStringAnnotation(span.key, span.value, start, end)
+        }
+        is BackgroundColorSpan -> {
+            addStyle(SpanStyle(background = Color(span.backgroundColor)), start, end)
+        }
+        is ForegroundColorSpan -> {
+            addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
+        }
+        is RelativeSizeSpan -> {
+            addStyle(SpanStyle(fontSize = span.sizeChange.em), start, end)
+        }
+        is StrikethroughSpan -> {
+            addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), start, end)
+        }
+        is StyleSpan -> {
+            span.toSpanStyle()?.let { addStyle(it, start, end) }
+        }
+        is SubscriptSpan -> {
+            addStyle(SpanStyle(baselineShift = BaselineShift.Subscript), start, end)
+        }
+        is SuperscriptSpan -> {
+            addStyle(SpanStyle(baselineShift = BaselineShift.Superscript), start, end)
+        }
+        is TypefaceSpan -> {
+            addStyle(span.toSpanStyle(), start, end)
+        }
+        is UnderlineSpan -> {
+            addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
+        }
+        is URLSpan -> {
+            span.url?.let {
+                addLink(LinkAnnotation.Url(it), start, end)
+            }
+        }
+    }
+}
+
+private fun AlignmentSpan.toParagraphStyle(): ParagraphStyle {
+    val alignment = when (this.alignment) {
+        Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start
+        Layout.Alignment.ALIGN_CENTER -> TextAlign.Center
+        Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End
+        else -> TextAlign.Unspecified
+    }
+    return ParagraphStyle(textAlign = alignment)
+}
+
+private fun StyleSpan.toSpanStyle(): SpanStyle? {
+    /** StyleSpan doc: styles are cumulative -- if both bold and italic are set in
+     * separate spans, or if the base style is bold and a span calls for italic,
+     * you get bold italic.  You can't turn off a style from the base style.
+     */
+    return when (style) {
+        Typeface.BOLD -> {
+            SpanStyle(fontWeight = FontWeight.Bold)
+        }
+        Typeface.ITALIC -> {
+            SpanStyle(fontStyle = FontStyle.Italic)
+        }
+        Typeface.BOLD_ITALIC -> {
+            SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
+        }
+        else -> null
+    }
+}
+
+private fun TypefaceSpan.toSpanStyle(): SpanStyle {
+    var fontFamily: FontFamily? = null
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+        fontFamily = Api28Impl.createFontFamilyFromTypeface(this)
+    }
+    if (fontFamily == null) fontFamily = when (family) {
+        null -> null
+        FontFamily.Cursive.name -> FontFamily.Cursive
+        FontFamily.Monospace.name -> FontFamily.Monospace
+        FontFamily.SansSerif.name -> FontFamily.SansSerif
+        FontFamily.Serif.name -> FontFamily.Serif
+        else -> FontFamily.Default
+    }
+    return SpanStyle(fontFamily = fontFamily)
+}
+
+@RequiresApi(28)
+private object Api28Impl {
+    @DoNotInline
+    fun createFontFamilyFromTypeface(typefaceSpan: TypefaceSpan) =
+        typefaceSpan.typeface?.let { FontFamily(it) }
+}
+
+private val TagHandler = object : TagHandler {
+    override fun handleTag(
+        opening: Boolean,
+        tag: String?,
+        output: Editable?,
+        xmlReader: XMLReader?
+    ) {
+        if (xmlReader == null || output == null) return
+
+        if (opening && tag == ContentHandlerReplacementTag) {
+            val currentContentHandler = xmlReader.contentHandler
+            xmlReader.contentHandler = AnnotationContentHandler(currentContentHandler, output)
+        }
+    }
+}
+
+private class AnnotationContentHandler(
+    private val contentHandler: ContentHandler,
+    private val output: Editable
+) : ContentHandler by contentHandler {
+    override fun startElement(uri: String?, localName: String?, qName: String?, atts: Attributes?) {
+        if (localName == AnnotationTag) {
+            atts?.let { handleAnnotationStart(it) }
+        } else {
+            contentHandler.startElement(uri, localName, qName, atts)
+        }
+    }
+
+    override fun endElement(uri: String?, localName: String?, qName: String?) {
+        if (localName == AnnotationTag) {
+            handleAnnotationEnd()
+        } else {
+            contentHandler.endElement(uri, localName, qName)
+        }
+    }
+
+    private fun handleAnnotationStart(attributes: Attributes) {
+        // Each annotation can have several key/value attributes. So for
+        // <annotation key1=value1 key2=value2>...<annotation>
+        // example we will add two [AnnotationSpan]s which we'll later read
+        for (i in 0 until attributes.length) {
+            val key = attributes.getLocalName(i).orEmpty()
+            val value = attributes.getValue(i).orEmpty()
+            if (key.isNotEmpty() && value.isNotEmpty()) {
+                val start = output.length
+                // add temporary AnnotationSpan to the output to read it when handling
+                // the closing tag
+                output.setSpan(AnnotationSpan(key, value), start, start, SPAN_MARK_MARK)
+            }
+        }
+    }
+
+    private fun handleAnnotationEnd() {
+        // iterate through all of the spans that we added when handling the opening tag. Calculate
+        // the true position of the span and make a replacement
+        output.getSpans(0, output.length, AnnotationSpan::class.java)
+            .filter { output.getSpanFlags(it) == SPAN_MARK_MARK }
+            .fastForEach { annotation ->
+                val start = output.getSpanStart(annotation)
+                val end = output.length
+
+                output.removeSpan(annotation)
+                // only add the annotation if there's a text in between the opening and closing tags
+                if (start != end) {
+                    output.setSpan(annotation, start, end, SPAN_EXCLUSIVE_EXCLUSIVE)
+                }
+            }
+    }
+}
+
+private class AnnotationSpan(val key: String, val value: String)
+
+/**
+ * This tag is added at the beginning of a string fed to the HTML parser in order to trigger
+ * a TagHandler's callback early on so we can replace the ContentHandler with our
+ * own [AnnotationContentHandler]. This is needed to handle the opening <annotation> tags since by
+ * the time TagHandler is triggered, the parser already visited and left the opening <annotation>
+ * tag which contains the attributes. Note that closing tag doesn't have the attributes and
+ * therefore not enough to construct the intermediate [AnnotationSpan] object that is later
+ * transformed into [AnnotatedString]'s string annotation.
+ */
+private const val ContentHandlerReplacementTag = "ContentHandlerReplacementTag"
+private const val AnnotationTag = "annotation"
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.kt
new file mode 100644
index 0000000..3332e6d
--- /dev/null
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.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.compose.ui.text
+
+/**
+ * Converts a string with HTML tags into [AnnotatedString].
+ *
+ * If you define your string in the resources, make sure to use HTML-escaped opening brackets
+ * "&lt;" instead of "<".
+ *
+ * For a list of supported tags go check
+ * [Styling with HTML markup](https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML)
+ * guide. Note that bullet lists and custom annotations are not **yet** available.
+ *
+ * Example of displaying styled string from resources
+ * @sample androidx.compose.ui.text.samples.AnnotatedStringFromHtml
+ */
+expect fun String.parseAsHtml(): AnnotatedString
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
similarity index 73%
copy from room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
copy to compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
index cc75983..20a8f27 100644
--- a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
+++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
@@ -14,6 +14,13 @@
  * limitations under the License.
  */
 
-package androidx.room.migration.bundle
-// empty file to trigger klib creation
-// see: https://youtrack.jetbrains.com/issue/KT-52344
+package androidx.compose.ui.text
+
+/**
+ * TBD: not yet implemented.
+ *
+ * Converts a string with HTML tags into [AnnotatedString].
+ */
+actual fun String.parseAsHtml(): AnnotatedString {
+    return AnnotatedString(this)
+}
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 67395d6..e0e50d7 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -71,7 +71,7 @@
                 api("androidx.annotation:annotation:1.1.0")
                 implementation(project(":compose:animation:animation"))
                 implementation("androidx.savedstate:savedstate-ktx:1.2.1")
-                implementation(project(":compose:material:material"))
+                implementation("androidx.compose.material:material:1.0.0")
                 implementation("androidx.activity:activity-compose:1.7.0")
                 implementation("androidx.lifecycle:lifecycle-common:2.6.1")
 
diff --git a/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
index e09ba8d..bb18d5b 100644
--- a/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
+++ b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
@@ -28,7 +28,6 @@
 import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.tooling.preview.PreviewDynamicColors
 import androidx.compose.ui.tooling.preview.PreviewFontScale
@@ -37,6 +36,7 @@
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 import androidx.compose.ui.tooling.preview.PreviewScreenSizes
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
 
 @Preview
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 0e97748..f0f1961 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -920,7 +920,7 @@
   public abstract sealed class VectorProperty<T> {
   }
 
-  public static final class VectorProperty.Fill extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush> {
+  public static final class VectorProperty.Fill extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush?> {
     field public static final androidx.compose.ui.graphics.vector.VectorProperty.Fill INSTANCE;
   }
 
@@ -952,7 +952,7 @@
     field public static final androidx.compose.ui.graphics.vector.VectorProperty.ScaleY INSTANCE;
   }
 
-  public static final class VectorProperty.Stroke extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush> {
+  public static final class VectorProperty.Stroke extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush?> {
     field public static final androidx.compose.ui.graphics.vector.VectorProperty.Stroke INSTANCE;
   }
 
@@ -2497,7 +2497,7 @@
     method public void getSlotsToRetain(androidx.compose.ui.layout.SubcomposeSlotReusePolicy.SlotIdsSet slotIds);
   }
 
-  public static final class SubcomposeSlotReusePolicy.SlotIdsSet implements java.util.Collection<java.lang.Object> kotlin.jvm.internal.markers.KMappedMarker {
+  public static final class SubcomposeSlotReusePolicy.SlotIdsSet implements java.util.Collection<java.lang.Object?> kotlin.jvm.internal.markers.KMappedMarker {
     method public void clear();
     method public java.util.Iterator<java.lang.Object?> iterator();
     method public boolean remove(Object? slotId);
@@ -2862,7 +2862,7 @@
     method public androidx.compose.ui.text.AnnotatedString? getText();
     method public default boolean hasClip();
     method public default boolean hasText();
-    method public default void setClip(androidx.compose.ui.platform.ClipEntry clipEntry);
+    method public default void setClip(androidx.compose.ui.platform.ClipEntry? clipEntry);
     method public void setText(androidx.compose.ui.text.AnnotatedString annotatedString);
     property public default android.content.ClipboardManager nativeClipboard;
   }
diff --git a/compose/ui/ui/api/res-current.txt b/compose/ui/ui/api/res-current.txt
index e69de29..ba71b41 100644
--- a/compose/ui/ui/api/res-current.txt
+++ b/compose/ui/ui/api/res-current.txt
@@ -0,0 +1 @@
+id hide_in_inspector_tag
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 54d037a..3a8128c 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -920,7 +920,7 @@
   public abstract sealed class VectorProperty<T> {
   }
 
-  public static final class VectorProperty.Fill extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush> {
+  public static final class VectorProperty.Fill extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush?> {
     field public static final androidx.compose.ui.graphics.vector.VectorProperty.Fill INSTANCE;
   }
 
@@ -952,7 +952,7 @@
     field public static final androidx.compose.ui.graphics.vector.VectorProperty.ScaleY INSTANCE;
   }
 
-  public static final class VectorProperty.Stroke extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush> {
+  public static final class VectorProperty.Stroke extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush?> {
     field public static final androidx.compose.ui.graphics.vector.VectorProperty.Stroke INSTANCE;
   }
 
@@ -2504,7 +2504,7 @@
     method public void getSlotsToRetain(androidx.compose.ui.layout.SubcomposeSlotReusePolicy.SlotIdsSet slotIds);
   }
 
-  public static final class SubcomposeSlotReusePolicy.SlotIdsSet implements java.util.Collection<java.lang.Object> kotlin.jvm.internal.markers.KMappedMarker {
+  public static final class SubcomposeSlotReusePolicy.SlotIdsSet implements java.util.Collection<java.lang.Object?> kotlin.jvm.internal.markers.KMappedMarker {
     method public void clear();
     method public java.util.Iterator<java.lang.Object?> iterator();
     method public boolean remove(Object? slotId);
@@ -2915,7 +2915,7 @@
     method public androidx.compose.ui.text.AnnotatedString? getText();
     method public default boolean hasClip();
     method public default boolean hasText();
-    method public default void setClip(androidx.compose.ui.platform.ClipEntry clipEntry);
+    method public default void setClip(androidx.compose.ui.platform.ClipEntry? clipEntry);
     method public void setText(androidx.compose.ui.text.AnnotatedString annotatedString);
     property public default android.content.ClipboardManager nativeClipboard;
   }
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 7ad3184..e1a0987 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -59,6 +59,8 @@
 import androidx.compose.ui.demos.gestures.HorizontalScrollersInVerticalScrollersDemo
 import androidx.compose.ui.demos.gestures.LongPressDragGestureFilterDemo
 import androidx.compose.ui.demos.gestures.LongPressGestureDetectorDemo
+import androidx.compose.ui.demos.gestures.MultiButtonsWithLoggingUsingOnClick
+import androidx.compose.ui.demos.gestures.MultiButtonsWithLoggingUsingPointerInput
 import androidx.compose.ui.demos.gestures.NestedLongPressDemo
 import androidx.compose.ui.demos.gestures.NestedPressingDemo
 import androidx.compose.ui.demos.gestures.NestedScrollDispatchDemo
@@ -109,6 +111,12 @@
                 ComposableDemo("Long Press Drag") { LongPressDragGestureFilterDemo() },
                 ComposableDemo("Scale") { ScaleGestureFilterDemo() },
                 ComposableDemo("Button/Meta State") { ButtonMetaStateDemo() },
+                ComposableDemo("Buttons with Logging using onClick") {
+                    MultiButtonsWithLoggingUsingOnClick()
+                },
+                ComposableDemo("Buttons with Logging using pointerInput") {
+                    MultiButtonsWithLoggingUsingPointerInput()
+                },
                 ComposableDemo("Event Types") { EventTypesDemo() },
             )
         ),
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/MultiButtonsWithLogging.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/MultiButtonsWithLogging.kt
new file mode 100644
index 0000000..c640def
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/MultiButtonsWithLogging.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.ui.demos.gestures
+
+import android.widget.Toast
+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.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+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.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun MultiButtonsWithLoggingUsingOnClick() {
+    var output by remember { mutableStateOf("") }
+
+    val context = LocalContext.current
+
+    Column(modifier = Modifier.fillMaxSize()) {
+        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+            Button(onClick = {
+                Toast.makeText(context, "Button 1 Clicked", Toast.LENGTH_SHORT).show()
+                val newString = "Button 1 Clicked\n$output"
+                output = newString
+            }) {
+                Text("Button 1")
+            }
+            Button(onClick = {
+                Toast.makeText(context, "Button 2 Clicked", Toast.LENGTH_SHORT).show()
+                val newString = "Button 2 Clicked\n$output"
+                output = newString
+            }) {
+                Text("Button 2")
+            }
+            Button(onClick = { output = "" }) {
+                Text("Clear Output")
+            }
+        }
+
+        Text(
+            modifier = Modifier.fillMaxWidth(),
+            text = output
+        )
+    }
+}
+
+@Composable
+fun MultiButtonsWithLoggingUsingPointerInput() {
+    var output by remember { mutableStateOf("") }
+
+    Column(modifier = Modifier.fillMaxSize()) {
+        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+            Text(
+                modifier = Modifier
+                    .padding(10.dp)
+                    .background(Color.Red)
+                    .pointerInput(Unit) {
+                        awaitPointerEventScope {
+                            while (true) {
+                                val pointerEvent = awaitPointerEvent()
+                                val newString = "Button 1 type: ${pointerEvent.type}\n$output"
+                                output = newString
+                            }
+                        }
+                    },
+                text = "Button 1"
+            )
+
+            Text(
+                modifier = Modifier
+                    .padding(10.dp)
+                    .background(Color.Red)
+                    .pointerInput(Unit) {
+                        awaitPointerEventScope {
+                            while (true) {
+                                val pointerEvent = awaitPointerEvent()
+                                val newString = "Button 2 type: ${pointerEvent.type}\n$output"
+                                output = newString
+                            }
+                        }
+                    },
+                text = "Button 2"
+            )
+
+            Text(
+                modifier = Modifier
+                    .padding(10.dp)
+                    .background(Color.Red)
+                    .pointerInput(Unit) {
+                        awaitPointerEventScope {
+
+                            while (true) {
+                                awaitPointerEvent()
+                                output = ""
+                            }
+                        }
+                    },
+
+                text = "Clear output"
+            )
+        }
+
+        Text(
+            modifier = Modifier.fillMaxWidth(),
+            text = output
+        )
+    }
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
index 5c7bddf..3b9a823 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
@@ -44,11 +44,11 @@
 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import kotlin.math.roundToInt
 
 @Suppress("SetTextI18n")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/contentcapture/ContentCaptureTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/contentcapture/ContentCaptureTest.kt
index 629ff4e..10940f9 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/contentcapture/ContentCaptureTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/contentcapture/ContentCaptureTest.kt
@@ -63,9 +63,11 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.isNull
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
@@ -140,7 +142,10 @@
                     Box(
                         Modifier
                             .size(10.dp)
-                            .semantics { text = AnnotatedString("foo") }
+                            .semantics {
+                                text = AnnotatedString("foo")
+                                testTag = "testTagFoo"
+                            }
                     )
                     Box(
                         Modifier
@@ -167,6 +172,10 @@
                 assertThat(firstValue).isEqualTo("foo")
                 assertThat(secondValue).isEqualTo("bar")
             }
+            with(argumentCaptor<String>()) {
+                verify(viewStructureCompat, times(1)).setId(anyInt(), isNull(), isNull(), capture())
+                assertThat(firstValue).isEqualTo("testTagFoo")
+            }
             verify(contentCaptureSessionCompat, times(0)).notifyViewsDisappeared(any())
             with(argumentCaptor<List<ViewStructure>>()) {
                 verify(contentCaptureSessionCompat, times(1)).notifyViewsAppeared(capture())
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
index d594371..94119f3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
@@ -21,6 +21,8 @@
 import android.os.Looper
 import android.view.InputDevice
 import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_BUTTON_PRESS
+import android.view.MotionEvent.ACTION_BUTTON_RELEASE
 import android.view.MotionEvent.ACTION_CANCEL
 import android.view.MotionEvent.ACTION_DOWN
 import android.view.MotionEvent.ACTION_HOVER_ENTER
@@ -1034,13 +1036,14 @@
         action: Int,
         layoutCoordinates: LayoutCoordinates,
         offset: Offset = Offset.Zero,
-        scrollDelta: Offset = Offset.Zero
+        scrollDelta: Offset = Offset.Zero,
+        eventTime: Int = 0
     ) {
         rule.runOnUiThread {
             val root = layoutCoordinates.findRootCoordinates()
             val pos = root.localPositionOf(layoutCoordinates, offset)
             val event = MotionEvent(
-                0,
+                eventTime,
                 action,
                 1,
                 0,
@@ -1092,13 +1095,14 @@
     private fun dispatchTouchEvent(
         action: Int,
         layoutCoordinates: LayoutCoordinates,
-        offset: Offset = Offset.Zero
+        offset: Offset = Offset.Zero,
+        eventTime: Int = 0
     ) {
         rule.runOnUiThread {
             val root = layoutCoordinates.findRootCoordinates()
             val pos = root.localPositionOf(layoutCoordinates, offset)
             val event = MotionEvent(
-                0,
+                eventTime,
                 action,
                 1,
                 0,
@@ -1183,7 +1187,7 @@
     }
 
     /*
-     * This is a simple test that makes sure a bad ACTION_OUTSIDE MotionEvent doesn't negatively
+     * Simple test that makes sure a bad ACTION_OUTSIDE MotionEvent doesn't negatively
      * impact Compose (b/299074463#comment31). (We actually ignore them in Compose.)
      * The event order of MotionEvents:
      *   1. Hover enter on box 1
@@ -1551,6 +1555,949 @@
     }
 
     /*
+     * Tests alternating between hover TOUCH events and touch events across multiple UI elements.
+     * Specifically, to recreate Talkback events.
+     *
+     * Important Note: Usually a hover MotionEvent sent from Android has the tool type set as
+     * [MotionEvent.TOOL_TYPE_MOUSE]. However, Talkback sets the tool type to
+     * [MotionEvent.TOOL_TYPE_FINGER]. We do that in this test by calling the
+     * [dispatchTouchEvent()] instead of [dispatchMouseEvent()].
+     *
+     * Specific events:
+     *  1. UI Element 1: ENTER (hover enter [touch])
+     *  2. UI Element 1: EXIT (hover exit [touch])
+     *  3. UI Element 1: PRESS (touch)
+     *  4. UI Element 1: RELEASE (touch)
+     *  5. UI Element 2: PRESS (touch)
+     *  6. UI Element 2: RELEASE (touch)
+     *
+     * Should NOT trigger any additional events (like an extra press or exit)!
+     */
+    @Test
+    fun alternatingHoverAndTouch_hoverUi1ToTouchUi1ToTouchUi2_shouldNotTriggerAdditionalEvents() {
+        // --> Arrange
+        var box1LayoutCoordinates: LayoutCoordinates? = null
+        var box2LayoutCoordinates: LayoutCoordinates? = null
+
+        val setUpFinishedLatch = CountDownLatch(4)
+
+        var eventTime = 100
+
+        // Events for Box 1
+        var box1HoverEnter = 0
+        var box1HoverExit = 0
+        var box1Down = 0
+        var box1Up = 0
+
+        // Events for Box 2
+        var box2Down = 0
+        var box2Up = 0
+
+        // All other events that should never be triggered in this test
+        var eventsThatShouldNotTrigger = false
+
+        var pointerEvent: PointerEvent? = null
+
+        rule.runOnUiThread {
+            container.setContent {
+                Column(
+                    Modifier
+                        .fillMaxSize()
+                        .onGloballyPositioned {
+                            setUpFinishedLatch.countDown()
+                        }
+                ) {
+                    // Box 1
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                box1LayoutCoordinates = it
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+
+                                        when (pointerEvent!!.type) {
+                                            PointerEventType.Enter -> {
+                                                ++box1HoverEnter
+                                            }
+
+                                            PointerEventType.Press -> {
+                                                ++box1Down
+                                            }
+
+                                            PointerEventType.Release -> {
+                                                ++box1Up
+                                            }
+
+                                            PointerEventType.Exit -> {
+                                                ++box1HoverExit
+                                            }
+
+                                            else -> {
+                                                eventsThatShouldNotTrigger = true
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                    ) { }
+
+                    // Box 2
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                box2LayoutCoordinates = it
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+
+                                        when (pointerEvent!!.type) {
+                                            PointerEventType.Press -> {
+                                                ++box2Down
+                                            }
+
+                                            PointerEventType.Release -> {
+                                                ++box2Up
+                                            }
+
+                                            else -> {
+                                                eventsThatShouldNotTrigger = true
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                    ) { }
+
+                    // Box 3
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+                                        // Should never do anything with this UI element.
+                                        eventsThatShouldNotTrigger = true
+                                    }
+                                }
+                            }
+                    ) { }
+                }
+            }
+        }
+        // Ensure Arrange (setup) step is finished
+        assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+        // --> Act + Assert (interwoven)
+        // Hover Enter on Box 1
+        dispatchTouchEvent(
+            action = ACTION_HOVER_ENTER,
+            layoutCoordinates = box1LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(0)
+            assertThat(box1Down).isEqualTo(0)
+            assertThat(box1Up).isEqualTo(0)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(0)
+            assertThat(box2Up).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+            assertHoverEvent(pointerEvent!!, isEnter = true)
+        }
+
+        // Hover Exit on Box 1
+        pointerEvent = null // Reset before each event
+        eventTime += 100
+        dispatchTouchEvent(
+            action = ACTION_HOVER_EXIT,
+            layoutCoordinates = box1LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+
+        rule.waitForFutureFrame(2)
+
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+            assertThat(box1Down).isEqualTo(0)
+            assertThat(box1Up).isEqualTo(0)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(0)
+            assertThat(box2Up).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        // Press on Box 1
+        pointerEvent = null // Reset before each event
+        eventTime += 100
+        dispatchTouchEvent(
+            action = ACTION_DOWN,
+            layoutCoordinates = box1LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+            assertThat(box1Down).isEqualTo(1)
+            assertThat(box1Up).isEqualTo(0)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(0)
+            assertThat(box2Up).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        // Release on Box 1
+        pointerEvent = null // Reset before each event
+        eventTime += 100
+        dispatchTouchEvent(
+            action = ACTION_UP,
+            layoutCoordinates = box1LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+            assertThat(box1Down).isEqualTo(1)
+            assertThat(box1Up).isEqualTo(1)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(0)
+            assertThat(box2Up).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        // Press on Box 2
+        pointerEvent = null // Reset before each event
+        eventTime += 100
+        dispatchTouchEvent(
+            action = ACTION_DOWN,
+            layoutCoordinates = box2LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+            assertThat(box1Down).isEqualTo(1)
+            assertThat(box1Up).isEqualTo(1)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(1)
+            assertThat(box2Up).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        // Press on Box 2
+        pointerEvent = null // Reset before each event
+        eventTime += 100
+        dispatchTouchEvent(
+            action = ACTION_UP,
+            layoutCoordinates = box2LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+            assertThat(box1Down).isEqualTo(1)
+            assertThat(box1Up).isEqualTo(1)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(1)
+            assertThat(box2Up).isEqualTo(1)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+    }
+
+    /*
+     * Tests alternating between hover TOUCH events and regular touch events across multiple
+     * UI elements. Specifically, to recreate Talkback events.
+     *
+     * Important Note: Usually a hover MotionEvent sent from Android has the tool type set as
+     * [MotionEvent.TOOL_TYPE_MOUSE]. However, Talkback sets the tool type to
+     * [MotionEvent.TOOL_TYPE_FINGER]. We do that in this test by calling the
+     * [dispatchTouchEvent()] instead of [dispatchMouseEvent()].
+     *
+     * Specific events:
+     *  1. UI Element 1: ENTER (hover enter [touch])
+     *  2. UI Element 1: EXIT (hover exit [touch])
+     *  5. UI Element 2: PRESS (touch)
+     *  6. UI Element 2: RELEASE (touch)
+     *
+     * Should NOT trigger any additional events (like an extra exit)!
+     */
+    @Test
+    fun alternatingHoverAndTouch_hoverUi1ToTouchUi2_shouldNotTriggerAdditionalEvents() {
+        // --> Arrange
+        var box1LayoutCoordinates: LayoutCoordinates? = null
+        var box2LayoutCoordinates: LayoutCoordinates? = null
+
+        val setUpFinishedLatch = CountDownLatch(4)
+
+        var eventTime = 100
+
+        // Events for Box 1
+        var box1HoverEnter = 0
+        var box1HoverExit = 0
+
+        // Events for Box 2
+        var box2Down = 0
+        var box2Up = 0
+
+        // All other events that should never be triggered in this test
+        var eventsThatShouldNotTrigger = false
+
+        var pointerEvent: PointerEvent? = null
+
+        rule.runOnUiThread {
+            container.setContent {
+                Column(
+                    Modifier
+                        .fillMaxSize()
+                        .onGloballyPositioned {
+                            setUpFinishedLatch.countDown()
+                        }
+                ) {
+                    // Box 1
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                box1LayoutCoordinates = it
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+
+                                        when (pointerEvent!!.type) {
+                                            PointerEventType.Enter -> {
+                                                ++box1HoverEnter
+                                            }
+
+                                            PointerEventType.Exit -> {
+                                                ++box1HoverExit
+                                            }
+
+                                            else -> {
+                                                eventsThatShouldNotTrigger = true
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                    ) { }
+
+                    // Box 2
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                box2LayoutCoordinates = it
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+
+                                        when (pointerEvent!!.type) {
+                                            PointerEventType.Press -> {
+                                                ++box2Down
+                                            }
+
+                                            PointerEventType.Release -> {
+                                                ++box2Up
+                                            }
+
+                                            else -> {
+                                                eventsThatShouldNotTrigger = true
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                    ) { }
+
+                    // Box 3
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+                                        // Should never do anything with this UI element.
+                                        eventsThatShouldNotTrigger = true
+                                    }
+                                }
+                            }
+                    ) { }
+                }
+            }
+        }
+        // Ensure Arrange (setup) step is finished
+        assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+        // --> Act + Assert (interwoven)
+        // Hover Enter on Box 1
+        dispatchTouchEvent(
+            action = ACTION_HOVER_ENTER,
+            layoutCoordinates = box1LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(0)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(0)
+            assertThat(box2Up).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+            assertHoverEvent(pointerEvent!!, isEnter = true)
+        }
+
+        // Hover Exit on Box 1
+        pointerEvent = null // Reset before each event
+        eventTime += 100
+        dispatchTouchEvent(
+            action = ACTION_HOVER_EXIT,
+            layoutCoordinates = box1LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+
+        rule.waitForFutureFrame(2)
+
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(0)
+            assertThat(box2Up).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        // Press on Box 2
+        pointerEvent = null // Reset before each event
+        eventTime += 100
+        dispatchTouchEvent(
+            action = ACTION_DOWN,
+            layoutCoordinates = box2LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(1)
+            assertThat(box2Up).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        // Press on Box 2
+        pointerEvent = null // Reset before each event
+        eventTime += 100
+        dispatchTouchEvent(
+            action = ACTION_UP,
+            layoutCoordinates = box2LayoutCoordinates!!,
+            eventTime = eventTime
+        )
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+
+            // Verify Box 2 events
+            assertThat(box2Down).isEqualTo(1)
+            assertThat(box2Up).isEqualTo(1)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+    }
+
+    /*
+     * Tests alternating hover TOUCH events across multiple UI elements. Specifically, to recreate
+     * Talkback events.
+     *
+     * Important Note: Usually a hover MotionEvent sent from Android has the tool type set as
+     * [MotionEvent.TOOL_TYPE_MOUSE]. However, Talkback sets the tool type to
+     * [MotionEvent.TOOL_TYPE_FINGER]. We do that in this test by calling the
+     * [dispatchTouchEvent()] instead of [dispatchMouseEvent()].
+     *
+     * Specific events:
+     *  1. UI Element 1: ENTER (hover enter [touch])
+     *  2. UI Element 1: EXIT (hover exit [touch])
+     *  5. UI Element 2: ENTER (hover enter [touch])
+     *  6. UI Element 2: EXIT (hover exit [touch])
+     *
+     * Should NOT trigger any additional events (like an extra exit)!
+     */
+    @Test
+    fun hoverEventsBetweenUIElements_hoverUi1ToHoverUi2_shouldNotTriggerAdditionalEvents() {
+        // --> Arrange
+        var box1LayoutCoordinates: LayoutCoordinates? = null
+        var box2LayoutCoordinates: LayoutCoordinates? = null
+
+        val setUpFinishedLatch = CountDownLatch(4)
+
+        // Events for Box 1
+        var box1HoverEnter = 0
+        var box1HoverExit = 0
+
+        // Events for Box 2
+        var box2HoverEnter = 0
+        var box2HoverExit = 0
+
+        // All other events that should never be triggered in this test
+        var eventsThatShouldNotTrigger = false
+
+        var pointerEvent: PointerEvent? = null
+
+        rule.runOnUiThread {
+            container.setContent {
+                Column(
+                    Modifier
+                        .fillMaxSize()
+                        .onGloballyPositioned {
+                            setUpFinishedLatch.countDown()
+                        }
+                ) {
+                    // Box 1
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                box1LayoutCoordinates = it
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+
+                                        when (pointerEvent!!.type) {
+                                            PointerEventType.Enter -> {
+                                                ++box1HoverEnter
+                                            }
+
+                                            PointerEventType.Exit -> {
+                                                ++box1HoverExit
+                                            }
+
+                                            else -> {
+                                                eventsThatShouldNotTrigger = true
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                    ) { }
+
+                    // Box 2
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                box2LayoutCoordinates = it
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+
+                                        when (pointerEvent!!.type) {
+                                            PointerEventType.Enter -> {
+                                                ++box2HoverEnter
+                                            }
+
+                                            PointerEventType.Exit -> {
+                                                ++box2HoverExit
+                                            }
+
+                                            else -> {
+                                                eventsThatShouldNotTrigger = true
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                    ) { }
+
+                    // Box 3
+                    Box(
+                        Modifier
+                            .size(50.dp)
+                            .onGloballyPositioned {
+                                setUpFinishedLatch.countDown()
+                            }
+                            .pointerInput(Unit) {
+                                awaitPointerEventScope {
+                                    while (true) {
+                                        pointerEvent = awaitPointerEvent()
+                                        // Should never do anything with this UI element.
+                                        eventsThatShouldNotTrigger = true
+                                    }
+                                }
+                            }
+                    ) { }
+                }
+            }
+        }
+        // Ensure Arrange (setup) step is finished
+        assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+        // --> Act + Assert (interwoven)
+        // Hover Enter on Box 1
+        dispatchTouchEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(0)
+
+            // Verify Box 2 events
+            assertThat(box2HoverEnter).isEqualTo(0)
+            assertThat(box2HoverExit).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+            assertHoverEvent(pointerEvent!!, isEnter = true)
+        }
+
+        // Hover Exit on Box 1
+        pointerEvent = null // Reset before each event
+        dispatchTouchEvent(ACTION_HOVER_EXIT, box1LayoutCoordinates!!)
+
+        rule.waitForFutureFrame(2)
+
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+            // Verify Box 2 events
+            assertThat(box2HoverEnter).isEqualTo(0)
+            assertThat(box2HoverExit).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        // Press on Box 2
+        pointerEvent = null // Reset before each event
+        dispatchTouchEvent(ACTION_HOVER_ENTER, box2LayoutCoordinates!!)
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+
+            // Verify Box 2 events
+            assertThat(box2HoverEnter).isEqualTo(1)
+            assertThat(box2HoverExit).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        // Press on Box 2
+        pointerEvent = null // Reset before each event
+        dispatchTouchEvent(ACTION_HOVER_EXIT, box2LayoutCoordinates!!)
+
+        rule.waitForFutureFrame(2)
+
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(box1HoverEnter).isEqualTo(1)
+            assertThat(box1HoverExit).isEqualTo(1)
+
+            // Verify Box 2 events
+            assertThat(box2HoverEnter).isEqualTo(1)
+            assertThat(box2HoverExit).isEqualTo(1)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+    }
+
+    /*
+     * Tests a full mouse event cycle from a press and release.
+     *
+     * Important Note: The pointer id should stay the same throughout all these events (part of the
+     * test).
+     *
+     * Specific MotionEvents:
+     *  1. UI Element 1: ENTER (hover enter [mouse])
+     *  2. UI Element 1: EXIT (hover exit [mouse]) - Doesn't trigger Compose PointerEvent
+     *  3. UI Element 1: PRESS (mouse)
+     *  4. UI Element 1: ACTION_BUTTON_PRESS (mouse)
+     *  5. UI Element 1: ACTION_BUTTON_RELEASE (mouse)
+     *  6. UI Element 1: RELEASE (mouse)
+     *  7. UI Element 1: ENTER (hover enter [mouse]) - Doesn't trigger Compose PointerEvent
+     *  8. UI Element 1: EXIT (hover enter [mouse])
+     *
+     * Should NOT trigger any additional events (like an extra press or exit)!
+     */
+    @Test
+    fun mouseEventsAndPointerIds_completeMouseEventCycle_pointerIdsShouldMatchAcrossAllEvents() {
+        // --> Arrange
+        var box1LayoutCoordinates: LayoutCoordinates? = null
+
+        val setUpFinishedLatch = CountDownLatch(1)
+
+        // Events for Box
+        var hoverEventCount = 0
+        var hoverExitCount = 0
+        var downCount = 0
+        // unknownCount covers both button action press and release from Android system for a
+        // mouse. These events happen between the normal press and release events.
+        var unknownCount = 0
+        var upCount = 0
+
+        // We want to assert that each updated pointer id matches the original pointer id that
+        // starts the sequence of MotionEvents.
+        var originalPointerId = -1L
+        var box1PointerId = -1L
+
+        // All other events that should never be triggered in this test
+        var eventsThatShouldNotTrigger = false
+
+        var pointerEvent: PointerEvent? = null
+
+        rule.runOnUiThread {
+            container.setContent {
+                Box(
+                    Modifier
+                        .size(50.dp)
+                        .onGloballyPositioned {
+                            box1LayoutCoordinates = it
+                            setUpFinishedLatch.countDown()
+                        }
+                        .pointerInput(Unit) {
+                            awaitPointerEventScope {
+                                while (true) {
+                                    pointerEvent = awaitPointerEvent()
+
+                                    if (originalPointerId < 0) {
+                                        originalPointerId = pointerEvent!!.changes[0].id.value
+                                    }
+
+                                    box1PointerId = pointerEvent!!.changes[0].id.value
+
+                                    when (pointerEvent!!.type) {
+                                        PointerEventType.Enter -> {
+                                            ++hoverEventCount
+                                        }
+
+                                        PointerEventType.Press -> {
+                                            ++downCount
+                                        }
+
+                                        PointerEventType.Release -> {
+                                            ++upCount
+                                        }
+
+                                        PointerEventType.Exit -> {
+                                            ++hoverExitCount
+                                        }
+
+                                        PointerEventType.Unknown -> {
+                                            ++unknownCount
+                                        }
+
+                                        else -> {
+                                            eventsThatShouldNotTrigger = true
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                ) { }
+            }
+        }
+        // Ensure Arrange (setup) step is finished
+        assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+        // --> Act + Assert (interwoven)
+        dispatchMouseEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
+        rule.runOnUiThread {
+            // Verify Box 1 events and pointer id
+            assertThat(originalPointerId).isEqualTo(box1PointerId)
+            assertThat(hoverEventCount).isEqualTo(1)
+            assertThat(hoverExitCount).isEqualTo(0)
+            assertThat(downCount).isEqualTo(0)
+            assertThat(unknownCount).isEqualTo(0)
+            assertThat(upCount).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+            assertHoverEvent(pointerEvent!!, isEnter = true)
+        }
+
+        pointerEvent = null // Reset before each event
+
+        // This will be interpreted as a synthetic event triggered by an ACTION_DOWN because we
+        // don't wait several frames before triggering the ACTION_DOWN. Thus, no hover exit is
+        // triggered.
+        dispatchMouseEvent(ACTION_HOVER_EXIT, box1LayoutCoordinates!!)
+
+        rule.runOnUiThread {
+            // Verify Box 1 events and pointer id
+            assertThat(originalPointerId).isEqualTo(box1PointerId)
+            assertThat(hoverEventCount).isEqualTo(1)
+            assertThat(hoverExitCount).isEqualTo(0)
+            assertThat(downCount).isEqualTo(0)
+            assertThat(unknownCount).isEqualTo(0)
+            assertThat(upCount).isEqualTo(0)
+
+            assertThat(pointerEvent).isNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        pointerEvent = null // Reset before each event
+        dispatchMouseEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+        rule.runOnUiThread {
+            assertThat(originalPointerId).isEqualTo(box1PointerId)
+            assertThat(hoverEventCount).isEqualTo(1)
+            assertThat(hoverExitCount).isEqualTo(0)
+            assertThat(downCount).isEqualTo(1)
+            assertThat(unknownCount).isEqualTo(0)
+            assertThat(upCount).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        pointerEvent = null // Reset before each event
+        dispatchMouseEvent(ACTION_BUTTON_PRESS, box1LayoutCoordinates!!)
+        rule.runOnUiThread {
+            // Verify Box 1 events
+            assertThat(originalPointerId).isEqualTo(box1PointerId)
+            assertThat(hoverEventCount).isEqualTo(1)
+            assertThat(hoverExitCount).isEqualTo(0)
+            assertThat(downCount).isEqualTo(1)
+            // unknownCount covers both button action press and release from Android system for a
+            // mouse. These events happen between the normal press and release events.
+            assertThat(unknownCount).isEqualTo(1)
+            assertThat(upCount).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        pointerEvent = null // Reset before each event
+        dispatchMouseEvent(ACTION_BUTTON_RELEASE, box1LayoutCoordinates!!)
+        rule.runOnUiThread {
+            assertThat(originalPointerId).isEqualTo(box1PointerId)
+            assertThat(hoverEventCount).isEqualTo(1)
+            assertThat(hoverExitCount).isEqualTo(0)
+            assertThat(downCount).isEqualTo(1)
+            // unknownCount covers both button action press and release from Android system for a
+            // mouse. These events happen between the normal press and release events.
+            assertThat(unknownCount).isEqualTo(2)
+            assertThat(upCount).isEqualTo(0)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        pointerEvent = null // Reset before each event
+        dispatchMouseEvent(ACTION_UP, box1LayoutCoordinates!!)
+        rule.runOnUiThread {
+            assertThat(originalPointerId).isEqualTo(box1PointerId)
+            assertThat(hoverEventCount).isEqualTo(1)
+            assertThat(hoverExitCount).isEqualTo(0)
+            assertThat(downCount).isEqualTo(1)
+            assertThat(unknownCount).isEqualTo(2)
+            assertThat(upCount).isEqualTo(1)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        pointerEvent = null // Reset before each event
+        // Compose already considered us as ENTERING the UI Element, so we don't need to trigger
+        // it again.
+        dispatchMouseEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
+
+        rule.runOnUiThread {
+            assertThat(originalPointerId).isEqualTo(box1PointerId)
+            assertThat(hoverEventCount).isEqualTo(1)
+            assertThat(hoverExitCount).isEqualTo(0)
+            assertThat(downCount).isEqualTo(1)
+            assertThat(unknownCount).isEqualTo(2)
+            assertThat(upCount).isEqualTo(1)
+
+            assertThat(pointerEvent).isNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+
+        pointerEvent = null // Reset before each event
+        dispatchMouseEvent(ACTION_HOVER_EXIT, box1LayoutCoordinates!!)
+
+        // Wait enough time for timeout on hover exit to trigger
+        rule.waitForFutureFrame(2)
+
+        rule.runOnUiThread {
+            assertThat(originalPointerId).isEqualTo(box1PointerId)
+            assertThat(hoverEventCount).isEqualTo(1)
+            assertThat(hoverExitCount).isEqualTo(1)
+            assertThat(downCount).isEqualTo(1)
+            assertThat(unknownCount).isEqualTo(2)
+            assertThat(upCount).isEqualTo(1)
+
+            assertThat(pointerEvent).isNotNull()
+            assertThat(eventsThatShouldNotTrigger).isFalse()
+        }
+    }
+
+    /*
      * Tests an ACTION_HOVER_EXIT MotionEvent is ignored in Compose when it proceeds an
      * ACTION_DOWN MotionEvent (in a measure of milliseconds only).
      *
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
index d382069..5fd4c2c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
@@ -417,7 +417,7 @@
             down = it.pressed,
             pressure = it.pressure,
             type = it.type,
-            issuesEnterExit = false,
+            activeHover = false,
             historical = emptyList()
         )
     }
@@ -451,7 +451,7 @@
         down = change.pressed,
         pressure = change.pressure,
         type = change.type,
-        issuesEnterExit = true,
+        activeHover = true,
         historical = emptyList()
     )
     val pointerEvent = PointerInputEvent(0L, listOf(pointer), createHoverMotionEvent(action, x, y))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
index cadfde0..af67ee0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
@@ -216,8 +216,8 @@
     @Test
     @SmallTest
     fun addedModifier() {
-        var latch1 = CountDownLatch(1)
-        var latch2 = CountDownLatch(1)
+        val latch1 = CountDownLatch(1)
+        val latch2 = CountDownLatch(1)
         var changedSize1 = IntSize.Zero
         var changedSize2 = IntSize.Zero
         var addModifier by mutableStateOf(false)
@@ -246,7 +246,6 @@
         assertEquals(10, changedSize1.height)
         assertEquals(10, changedSize1.width)
 
-        latch1 = CountDownLatch(1)
         addModifier = true
 
         // We've added an onSizeChanged modifier, so it must trigger another size change
@@ -257,6 +256,133 @@
 
     @Test
     @SmallTest
+    fun addedModifierNode() {
+        val sizeLatch1 = CountDownLatch(1)
+        val sizeLatch2 = CountDownLatch(1)
+        val placedLatch1 = CountDownLatch(1)
+        val placedLatch2 = CountDownLatch(1)
+        var changedSize1 = IntSize.Zero
+        var changedSize2 = IntSize.Zero
+        var addModifier by mutableStateOf(false)
+
+        val node = object : LayoutAwareModifierNode, Modifier.Node() {
+            override fun onRemeasured(size: IntSize) {
+                changedSize1 = size
+                sizeLatch1.countDown()
+            }
+            override fun onPlaced(coordinates: LayoutCoordinates) {
+                placedLatch1.countDown()
+            }
+        }
+
+        val node2 = object : LayoutAwareModifierNode, Modifier.Node() {
+            override fun onRemeasured(size: IntSize) {
+                changedSize2 = size
+                sizeLatch2.countDown()
+            }
+            override fun onPlaced(coordinates: LayoutCoordinates) {
+                placedLatch2.countDown()
+            }
+        }
+
+        rule.runOnUiThread {
+            activity.setContent {
+                with(LocalDensity.current) {
+                    val mod = if (addModifier) Modifier.elementFor(node2) else Modifier
+                    Box(
+                        Modifier.padding(10.toDp()).elementFor(node).then(mod)
+                    ) {
+                        Box(Modifier.requiredSize(10.toDp()))
+                    }
+                }
+            }
+        }
+
+        // Initial setting will call onRemeasured and onPlaced
+        assertTrue(sizeLatch1.await(1, TimeUnit.SECONDS))
+        assertTrue(placedLatch1.await(1, TimeUnit.SECONDS))
+        assertEquals(10, changedSize1.height)
+        assertEquals(10, changedSize1.width)
+
+        addModifier = true
+
+        // We've added a node, so it must trigger onRemeasured and onPlaced on the new node
+        assertTrue(sizeLatch2.await(1, TimeUnit.SECONDS))
+        assertTrue(placedLatch2.await(1, TimeUnit.SECONDS))
+        assertEquals(10, changedSize2.height)
+        assertEquals(10, changedSize2.width)
+    }
+
+    @Test
+    @SmallTest
+    fun lazilyDelegatedModifierNode() {
+        val sizeLatch1 = CountDownLatch(1)
+        val sizeLatch2 = CountDownLatch(1)
+        val placedLatch1 = CountDownLatch(1)
+        val placedLatch2 = CountDownLatch(1)
+        var changedSize1 = IntSize.Zero
+        var changedSize2 = IntSize.Zero
+
+        val node = object : LayoutAwareModifierNode, Modifier.Node() {
+            override fun onRemeasured(size: IntSize) {
+                changedSize1 = size
+                sizeLatch1.countDown()
+            }
+
+            override fun onPlaced(coordinates: LayoutCoordinates) {
+                placedLatch1.countDown()
+            }
+        }
+
+        val node2 = object : DelegatingNode() {
+            fun addDelegate() {
+                delegate(
+                    object : LayoutAwareModifierNode, Modifier.Node() {
+                        override fun onRemeasured(size: IntSize) {
+                            changedSize2 = size
+                            sizeLatch2.countDown()
+                        }
+
+                        override fun onPlaced(coordinates: LayoutCoordinates) {
+                            placedLatch2.countDown()
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.runOnUiThread {
+            activity.setContent {
+                with(LocalDensity.current) {
+                    val mod = Modifier.elementFor(node2)
+                    Box(
+                        Modifier.padding(10.toDp()).elementFor(node).then(mod)
+                    ) {
+                        Box(Modifier.requiredSize(10.toDp()))
+                    }
+                }
+            }
+        }
+
+        // Initial setting will call onRemeasured and onPlaced
+        assertTrue(sizeLatch1.await(1, TimeUnit.SECONDS))
+        assertTrue(placedLatch1.await(1, TimeUnit.SECONDS))
+        assertEquals(10, changedSize1.height)
+        assertEquals(10, changedSize1.width)
+
+        rule.runOnUiThread {
+            node2.addDelegate()
+        }
+
+        // We've delegated to a node, so it must trigger onRemeasured and onPlaced on the new node
+        assertTrue(sizeLatch2.await(1, TimeUnit.SECONDS))
+        assertTrue(placedLatch2.await(1, TimeUnit.SECONDS))
+        assertEquals(10, changedSize2.height)
+        assertEquals(10, changedSize2.width)
+    }
+
+    @Test
+    @SmallTest
     fun modifierIsReturningEqualObjectForTheSameLambda() {
         val lambda: (IntSize) -> Unit = { }
         assertEquals(Modifier.onSizeChanged(lambda), Modifier.onSizeChanged(lambda))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index a85b878..95e0379 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -2859,7 +2859,7 @@
     private fun SemanticsNodeInteraction.assertIsDetached() {
         assertDoesNotExist()
         // we want to verify the node is not deactivated, but such API does not exist yet
-        expectAssertionError(true) {
+        expectAssertionError {
             assertIsDeactivated()
         }
     }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
index 6c8d88b..8ea1a4c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
@@ -19,8 +19,8 @@
 import androidx.activity.compose.setContent
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import java.util.concurrent.CountDownLatch
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
index 9d79d60..73ed014 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
@@ -19,8 +19,8 @@
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import java.util.concurrent.CountDownLatch
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
index 497f8be..cae14f2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
@@ -22,11 +22,11 @@
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
 import androidx.fragment.app.FragmentContainerView
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import java.util.concurrent.CountDownLatch
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
index 0ffc44f..2d1dcff 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
@@ -36,10 +36,12 @@
 import androidx.compose.ui.text.withStyle
 import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
@@ -296,6 +298,35 @@
         verify(clipboardManager, times(1)).setPrimaryClip(clipData)
     }
 
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun setPrimaryClip_callsClearPrimaryClip_ifNull_above28() {
+        val clipboardManager = mock<ClipboardManager>()
+        val subject = AndroidClipboardManager(clipboardManager)
+
+        subject.setClip(null)
+
+        verify(clipboardManager, times(1)).clearPrimaryClip()
+    }
+
+    @SdkSuppress(maxSdkVersion = 27)
+    @Test
+    fun setPrimaryClip_callsClearPrimaryClip_ifNull_below27() {
+        val clipboardManager = mock<ClipboardManager>()
+        val subject = AndroidClipboardManager(clipboardManager)
+
+        subject.setClip(null)
+
+        val argumentCaptor = argumentCaptor<ClipData>()
+        verify(clipboardManager, times(1))
+            .setPrimaryClip(argumentCaptor.capture())
+
+        assertThat(argumentCaptor.lastValue.itemCount).isEqualTo(1)
+        assertThat(argumentCaptor.lastValue.getItemAt(0).uri).isEqualTo(null)
+        assertThat(argumentCaptor.lastValue.getItemAt(0).intent).isEqualTo(null)
+        assertThat(argumentCaptor.lastValue.getItemAt(0).text).isEqualTo("")
+    }
+
     @OptIn(ExperimentalComposeUiApi::class)
     @Test
     fun firstUriOrNull_returnsFirstItem_ifNotNull() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
index b1c2668..4f703cb 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
@@ -29,6 +29,7 @@
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.InternalComposeUiApi
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.rules.activityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 9c7155d1..3e56cd1 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -74,7 +74,6 @@
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.compose.ui.platform.findViewTreeCompositionContext
@@ -108,6 +107,7 @@
 import androidx.lifecycle.Lifecycle.Event.ON_STOP
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.savedstate.SavedStateRegistry
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
index 9ff7b8a..23e60dd 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
@@ -386,6 +386,10 @@
             "android.view.contentcapture.EventTimestamp",
             currentSemanticsNodesSnapshotTimestampMillis)
 
+        configuration.getOrNull(SemanticsProperties.TestTag)?.let {
+            // Treat test tag as resourceId
+            structure.setId(id, null, null, it)
+        }
         configuration.getOrNull(SemanticsProperties.Text)?.let {
             structure.setClassName("android.widget.TextView")
             structure.setText(it.fastJoinToString("\n"))
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerEvent.android.kt
index e8433af..ebe21d4 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerEvent.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerEvent.android.kt
@@ -29,10 +29,10 @@
     val motionEvent: MotionEvent
         get() = pointerInputEvent.motionEvent
 
-    actual fun issuesEnterExitEvent(pointerId: PointerId): Boolean =
+    actual fun activeHoverEvent(pointerId: PointerId): Boolean =
         pointerInputEvent.pointers.fastFirstOrNull {
             it.id == pointerId
-        }?.issuesEnterExit ?: false
+        }?.activeHover ?: false
 
     actual var suppressMovementConsumption: Boolean = false
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
index 1882f1c..114f1e5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
@@ -54,7 +54,8 @@
      */
     @VisibleForTesting
     internal val motionEventToComposePointerIdMap = SparseLongArray()
-    private val canHover = SparseBooleanArray()
+
+    private val activeHoverIds = SparseBooleanArray()
 
     private val pointers = mutableListOf<PointerInputEventData>()
 
@@ -88,20 +89,21 @@
         val action = motionEvent.actionMasked
         if (action == ACTION_CANCEL || action == ACTION_OUTSIDE) {
             motionEventToComposePointerIdMap.clear()
-            canHover.clear()
+            activeHoverIds.clear()
             return null
         }
         clearOnDeviceChange(motionEvent)
 
         addFreshIds(motionEvent)
 
-        val isHover = action == ACTION_HOVER_EXIT || action == ACTION_HOVER_MOVE ||
-            action == ACTION_HOVER_ENTER
+        val isHover = action == ACTION_HOVER_ENTER ||
+            action == ACTION_HOVER_MOVE || action == ACTION_HOVER_EXIT
+
         val isScroll = action == ACTION_SCROLL
 
         if (isHover) {
             val hoverId = motionEvent.getPointerId(motionEvent.actionIndex)
-            canHover.put(hoverId, true)
+            activeHoverIds.put(hoverId, true)
         }
 
         val upIndex = when (action) {
@@ -143,7 +145,7 @@
      * be considered ended.
      */
     fun endStream(pointerId: Int) {
-        canHover.delete(pointerId)
+        activeHoverIds.delete(pointerId)
         motionEventToComposePointerIdMap.delete(pointerId)
     }
 
@@ -166,7 +168,7 @@
                 if (motionEventToComposePointerIdMap.indexOfKey(pointerId) < 0) {
                     motionEventToComposePointerIdMap.put(pointerId, nextId++)
                     if (motionEvent.getToolType(actionIndex) == TOOL_TYPE_MOUSE) {
-                        canHover.put(pointerId, true)
+                        activeHoverIds.put(pointerId, true)
                     }
                 }
             }
@@ -183,9 +185,9 @@
             ACTION_UP -> {
                 val actionIndex = motionEvent.actionIndex
                 val pointerId = motionEvent.getPointerId(actionIndex)
-                if (!canHover.get(pointerId, false)) {
+                if (!activeHoverIds.get(pointerId, false)) {
                     motionEventToComposePointerIdMap.delete(pointerId)
-                    canHover.delete(pointerId)
+                    activeHoverIds.delete(pointerId)
                 }
             }
         }
@@ -198,7 +200,7 @@
                 val pointerId = motionEventToComposePointerIdMap.keyAt(i)
                 if (!motionEvent.hasPointerId(pointerId)) {
                     motionEventToComposePointerIdMap.removeAt(i)
-                    canHover.delete(pointerId)
+                    activeHoverIds.delete(pointerId)
                 }
             }
         }
@@ -240,7 +242,7 @@
         if (toolType != previousToolType || source != previousSource) {
             previousToolType = toolType
             previousSource = source
-            canHover.clear()
+            activeHoverIds.clear()
             motionEventToComposePointerIdMap.clear()
         }
     }
@@ -323,7 +325,7 @@
             Offset.Zero
         }
 
-        val issuesEnterExit = canHover.get(motionEvent.getPointerId(index), false)
+        val activeHover = activeHoverIds.get(motionEvent.getPointerId(index), false)
         return PointerInputEventData(
             pointerId,
             motionEvent.eventTime,
@@ -332,7 +334,7 @@
             pressed,
             pressure,
             toolType,
-            issuesEnterExit,
+            activeHover,
             historical,
             scrollDelta,
             originalPositionEventPosition,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
index 27ee806..abce6ff 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
@@ -104,7 +104,7 @@
                     change.pressed,
                     change.pressure,
                     change.type,
-                    this.internalPointerEvent?.issuesEnterExitEvent(change.id) == true
+                    this.internalPointerEvent?.activeHoverEvent(change.id) == true
                 )
             }
 
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidClipboardManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidClipboardManager.android.kt
index 2c94b70..f450a04 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidClipboardManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidClipboardManager.android.kt
@@ -19,11 +19,14 @@
 import android.content.ClipData
 import android.content.ClipDescription
 import android.content.Context
+import android.os.Build
 import android.os.Parcel
 import android.text.Annotation
 import android.text.SpannableString
 import android.text.Spanned
 import android.util.Base64
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
@@ -86,9 +89,16 @@
         return clipboardManager.primaryClipDescription?.let(::ClipMetadata)
     }
 
-    override fun setClip(clipEntry: ClipEntry) {
-        // We ignore the clipDescription parameter on Android because clipEntry comes with one.
-        clipboardManager.setPrimaryClip(clipEntry.clipData)
+    override fun setClip(clipEntry: ClipEntry?) {
+        if (clipEntry == null) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                Api28ClipboardManagerClipClear.clearPrimaryClip(clipboardManager)
+            } else {
+                clipboardManager.setPrimaryClip(ClipData.newPlainText("", ""))
+            }
+        } else {
+            clipboardManager.setPrimaryClip(clipEntry.clipData)
+        }
     }
 
     override fun hasClip(): Boolean = clipboardManager.hasPrimaryClip()
@@ -118,6 +128,16 @@
 
 actual typealias NativeClipboard = android.content.ClipboardManager
 
+@RequiresApi(28)
+private object Api28ClipboardManagerClipClear {
+
+    @DoNotInline
+    @JvmStatic
+    fun clearPrimaryClip(clipboardManager: android.content.ClipboardManager) {
+        clipboardManager.clearPrimaryClip()
+    }
+}
+
 internal fun CharSequence?.convertToAnnotatedString(): AnnotatedString? {
     if (this == null) return null
     if (this !is Spanned) {
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 94c66ae..923af73 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
@@ -1803,6 +1803,61 @@
                     sendSimulatedEvent(motionEvent, ACTION_HOVER_ENTER, motionEvent.eventTime)
                 }
                 lastEvent?.recycle()
+
+                // If the previous MotionEvent was an ACTION_HOVER_EXIT, we need to check if it
+                // was a synthetic MotionEvent generated by the platform for an ACTION_DOWN event
+                // or not.
+                //
+                // If it was synthetic, we do nothing, because we want to keep the existing cache
+                // of "Hit" Modifier.Node(s) from the previous hover events, so we can reuse them
+                // once an ACTION_UP event is triggered and we return to the same hover state
+                // (cache improves performance for this frequent event sequence with a mouse).
+                //
+                // If it was NOT synthetic, we end the event stream in MotionEventAdapter and clear
+                // the hit cache used in PointerInputEventProcessor (specifically, the
+                // HitPathTracker cache inside PointerInputEventProcessor), so events in this new
+                // stream do not trigger Modifier.Node(s) hit by the previous stream.
+                if (previousMotionEvent?.action == ACTION_HOVER_EXIT) {
+                    val previousEventDefaultPointerId =
+                        previousMotionEvent?.getPointerId(0) ?: -1
+
+                    // New ACTION_HOVER_ENTER, so this should be considered a new stream
+                    if (motionEvent.action == ACTION_HOVER_ENTER && motionEvent.historySize == 0) {
+                        if (previousEventDefaultPointerId >= 0) {
+                            motionEventAdapter.endStream(previousEventDefaultPointerId)
+                        }
+                    } else if (motionEvent.action == ACTION_DOWN && motionEvent.historySize == 0) {
+                        val previousX = previousMotionEvent?.x ?: Float.NaN
+                        val previousY = previousMotionEvent?.y ?: Float.NaN
+
+                        val currentX = motionEvent.x
+                        val currentY = motionEvent.y
+
+                        val previousAndCurrentCoordinatesDoNotMatch =
+                            (previousX != currentX || previousY != currentY)
+
+                        val previousEventTime = previousMotionEvent?.eventTime ?: -1L
+
+                        val previousAndCurrentEventTimesDoNotMatch =
+                            previousEventTime != motionEvent.eventTime
+
+                        // A synthetically created Hover Exit event will always have the same x,
+                        // y, and timestamp as the down event it proceeds.
+                        val previousHoverEventWasNotSyntheticallyProducedFromADownEvent =
+                            previousAndCurrentCoordinatesDoNotMatch ||
+                            previousAndCurrentEventTimesDoNotMatch
+
+                        if (previousHoverEventWasNotSyntheticallyProducedFromADownEvent) {
+                            // This should be considered a new stream, and we should
+                            // reset everything.
+                            if (previousEventDefaultPointerId >= 0) {
+                                motionEventAdapter.endStream(previousEventDefaultPointerId)
+                            }
+                            pointerInputEventProcessor.clearPreviouslyHitModifierNodes()
+                        }
+                    }
+                }
+
                 previousMotionEvent = MotionEvent.obtainNoHistory(motionEvent)
 
                 sendMotionEvent(motionEvent)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
index 118695c..6716cc4 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
@@ -19,8 +19,8 @@
 import android.content.Context
 import android.view.View
 import android.view.ViewGroup
+import androidx.compose.ui.R
 import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.graphics.R
 import androidx.compose.ui.graphics.nativeCanvas
 
 /**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index 47250c8..744a9a9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -47,13 +47,13 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.ViewRootForInspector
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.savedstate.SavedStateRegistryOwner
 
diff --git a/compose/ui/ui/src/androidMain/res/values/ids.xml b/compose/ui/ui/src/androidMain/res/values/ids.xml
index 140ecaf..721b41d 100644
--- a/compose/ui/ui/src/androidMain/res/values/ids.xml
+++ b/compose/ui/ui/src/androidMain/res/values/ids.xml
@@ -52,5 +52,6 @@
     <item name="inspection_slot_table_set" type="id" />
     <item name="androidx_compose_ui_view_composition_context" type="id" />
     <item name="compose_view_saveable_id_tag" type="id" />
+    <item name="hide_in_inspector_tag" type="id" />
     <item name="consume_window_insets_tag" type="id" />
 </resources>
diff --git a/compose/ui/ui/src/androidMain/res/values/public.xml b/compose/ui/ui/src/androidMain/res/values/public.xml
index 4d54254..969bb58 100644
--- a/compose/ui/ui/src/androidMain/res/values/public.xml
+++ b/compose/ui/ui/src/androidMain/res/values/public.xml
@@ -15,4 +15,5 @@
   -->
 
 <resources>
+    <public name="hide_in_inspector_tag" type="id" />
 </resources>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/platform/ClipboardManagerTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/platform/ClipboardManagerTest.kt
index 6c31ebf..734d0d2 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/platform/ClipboardManagerTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/platform/ClipboardManagerTest.kt
@@ -57,5 +57,5 @@
 
     override fun hasClip(): Boolean = false
 
-    override fun setClip(clipEntry: ClipEntry) = Unit
+    override fun setClip(clipEntry: ClipEntry?) = Unit
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index d645907..8613ddd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -112,6 +112,10 @@
         return dispatchHit
     }
 
+    fun clearPreviouslyHitModifierNodeCache() {
+        root.clear()
+    }
+
     /**
      * Dispatches cancel events to all tracked [PointerInputFilter]s to notify them that
      * [PointerInputFilter.onPointerEvent] will not be called again until all pointers have been
@@ -120,7 +124,7 @@
      */
     fun processCancel() {
         root.dispatchCancel()
-        root.clear()
+        clearPreviouslyHitModifierNodeCache()
     }
 
     /**
@@ -430,17 +434,19 @@
             changesList.add(relevantChanges.valueAt(i))
         }
         val event = PointerEvent(changesList, internalPointerEvent)
-        val enterExitChange = event.changes.fastFirstOrNull {
-            internalPointerEvent.issuesEnterExitEvent(it.id)
+
+        val activeHoverChange = event.changes.fastFirstOrNull {
+            internalPointerEvent.activeHoverEvent(it.id)
         }
-        if (enterExitChange != null) {
+
+        if (activeHoverChange != null) {
             if (!isInBounds) {
                 isIn = false
-            } else if (!isIn && (enterExitChange.pressed || enterExitChange.previousPressed)) {
+            } else if (!isIn && (activeHoverChange.pressed || activeHoverChange.previousPressed)) {
                 // We have to recalculate isIn because we didn't redo hit testing
                 val size = coordinates!!.size
                 @Suppress("DEPRECATION")
-                isIn = !enterExitChange.isOutOfBounds(size)
+                isIn = !activeHoverChange.isOutOfBounds(size)
             }
             if (isIn != wasIn &&
                 (
@@ -456,7 +462,7 @@
                 }
             } else if (event.type == PointerEventType.Enter && wasIn && !hasExited) {
                 event.type = PointerEventType.Move // We already knew that it was in.
-            } else if (event.type == PointerEventType.Exit && isIn && enterExitChange.pressed) {
+            } else if (event.type == PointerEventType.Exit && isIn && activeHoverChange.pressed) {
                 event.type = PointerEventType.Move // We are still in.
             }
         }
@@ -537,11 +543,17 @@
         wasIn = isIn
 
         event.changes.fastForEach { change ->
-            // If the pointer is released and doesn't support hover OR
-            // the pointer supports over and is released outside the area
-            val remove = !change.pressed &&
-                (!internalPointerEvent.issuesEnterExitEvent(change.id) || !isIn)
-            if (remove) {
+            // There are two scenarios where we need to remove the pointerIds:
+            //   1. Pointer is released AND event stream doesn't have an active hover.
+            //   2. Pointer is released AND is released outside the area.
+            val released = !change.pressed
+            val nonHoverEventStream = !internalPointerEvent.activeHoverEvent(change.id)
+            val outsideArea = !isIn
+
+            val removePointerId =
+                (released && nonHoverEventStream) || (released && outsideArea)
+
+            if (removePointerId) {
                 pointerIds.remove(change.id)
             }
         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
index a7a1a96..28b0bd7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
@@ -48,7 +48,7 @@
     val down: Boolean,
     val pressure: Float,
     val type: PointerType,
-    val issuesEnterExit: Boolean = false,
+    val activeHover: Boolean = false,
     val historical: List<HistoricalChange> = mutableListOf(),
     val scrollDelta: Offset = Offset.Zero,
     val originalEventPosition: Offset = Offset.Zero,
@@ -73,5 +73,5 @@
      * return that the position change was consumed because of this.
      */
     var suppressMovementConsumption: Boolean
-    fun issuesEnterExitEvent(pointerId: PointerId): Boolean
+    fun activeHoverEvent(pointerId: PointerId): Boolean
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index d578e14..4230b9c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -147,6 +147,14 @@
             hitPathTracker.processCancel()
         }
     }
+
+    /**
+     * In some cases we need to clear the HIT Modifier.Node(s) cached from previous events because
+     * they are no longer relevant.
+     */
+    fun clearPreviouslyHitModifierNodes() {
+        hitPathTracker.clearPreviouslyHitModifierNodeCache()
+    }
 }
 
 /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
index 6ebe14f..91fe684 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
@@ -20,9 +20,9 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.IntSize
 
 /**
@@ -44,27 +44,15 @@
 @Stable
 fun Modifier.onSizeChanged(
     onSizeChanged: (IntSize) -> Unit
-) = this.then(
-    OnSizeChangedModifier(
-        onSizeChanged = onSizeChanged,
-        inspectorInfo = debugInspectorInfo {
-            name = "onSizeChanged"
-            properties["onSizeChanged"] = onSizeChanged
-        }
-    )
-)
+) = this.then(OnSizeChangedModifier(onSizeChanged = onSizeChanged))
 
 private class OnSizeChangedModifier(
-    val onSizeChanged: (IntSize) -> Unit,
-    inspectorInfo: InspectorInfo.() -> Unit
-) : OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
-    private var previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
+    private val onSizeChanged: (IntSize) -> Unit
+) : ModifierNodeElement<OnSizeChangedNode>() {
+    override fun create(): OnSizeChangedNode = OnSizeChangedNode(onSizeChanged)
 
-    override fun onRemeasured(size: IntSize) {
-        if (previousSize != size) {
-            onSizeChanged(size)
-            previousSize = size
-        }
+    override fun update(node: OnSizeChangedNode) {
+        node.update(onSizeChanged)
     }
 
     override fun equals(other: Any?): Boolean {
@@ -77,6 +65,33 @@
     override fun hashCode(): Int {
         return onSizeChanged.hashCode()
     }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "onSizeChanged"
+        properties["onSizeChanged"] = onSizeChanged
+    }
+}
+
+private class OnSizeChangedNode(
+    private var onSizeChanged: (IntSize) -> Unit
+) : Modifier.Node(), LayoutAwareModifierNode {
+    // When onSizeChanged changes, we want to invalidate so onRemeasured is called again
+    override val shouldAutoInvalidate: Boolean = true
+    private var previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
+
+    fun update(onSizeChanged: (IntSize) -> Unit) {
+        this.onSizeChanged = onSizeChanged
+        // Reset the previous size, so when onSizeChanged changes the new lambda gets invoked,
+        // matching previous behavior
+        previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
+    }
+
+    override fun onRemeasured(size: IntSize) {
+        if (previousSize != size) {
+            onSizeChanged(size)
+            previousSize = size
+        }
+    }
 }
 
 /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
index 2eaa226..df50f30 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
@@ -456,6 +456,8 @@
                 layoutNode.replace()
                 onPositionedDispatcher.onNodePositioned(layoutNode)
             }
+
+            drainPostponedMeasureRequests()
         }
         callOnLayoutCompletedListeners()
     }
@@ -568,23 +570,26 @@
                     }
                 }
             }
-            // execute postponed `onRequestMeasure`
-            if (postponedMeasureRequests.isNotEmpty()) {
-                postponedMeasureRequests.forEach { request ->
-                    if (request.node.isAttached) {
-                        if (!request.isLookahead) {
-                            requestRemeasure(request.node, request.isForced)
-                        } else {
-                            requestLookaheadRemeasure(request.node, request.isForced)
-                        }
-                    }
-                }
-                postponedMeasureRequests.clear()
-            }
+            drainPostponedMeasureRequests()
         }
         return sizeChanged
     }
 
+    private fun drainPostponedMeasureRequests() {
+        if (postponedMeasureRequests.isNotEmpty()) {
+            postponedMeasureRequests.forEach { request ->
+                if (request.node.isAttached) {
+                    if (!request.isLookahead) {
+                        requestRemeasure(request.node, request.isForced)
+                    } else {
+                        requestLookaheadRemeasure(request.node, request.isForced)
+                    }
+                }
+            }
+            postponedMeasureRequests.clear()
+        }
+    }
+
     /**
      * Remeasures [layoutNode] if it has [LayoutNode.measurePending] or
      * [LayoutNode.lookaheadMeasurePending].
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 f55d67f..e669d1e 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
@@ -268,6 +268,9 @@
             coordinator.onRelease()
         }
     }
+    if (Nodes.LayoutAware in selfKindSet && node is LayoutAwareModifierNode) {
+        node.requireLayoutNode().invalidateMeasurements()
+    }
     if (Nodes.GlobalPositionAware in selfKindSet && node is GlobalPositionAwareModifierNode) {
         node.requireLayoutNode().invalidateOnPositioned()
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ClipboardManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ClipboardManager.kt
index 9100166..c96125f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ClipboardManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ClipboardManager.kt
@@ -45,7 +45,8 @@
     fun hasText(): Boolean = getText()?.isNotEmpty() == true
 
     /**
-     * Returns the primary clipboard entry that's provided by the platform's ClipboardManager.
+     * Returns the clipboard entry that's provided by the platform's ClipboardManager.
+     *
      * This item can include arbitrary content like images, videos, or any data that may be provided
      * through a mediator. Returned entry may contain multiple items with different types.
      *
@@ -55,7 +56,7 @@
     fun getClip(): ClipEntry? = null
 
     /**
-     * Returns a [ClipMetadata] which describes the primary clip entry. This is an ideal way to
+     * Returns a [ClipMetadata] which describes the existing clip entry. This is an ideal way to
      * check whether to accept or reject what may be pasted from the clipboard without explicitly
      * reading the content.
      *
@@ -66,15 +67,16 @@
     /**
      * Puts the given [clipEntry] in platform's ClipboardManager.
      *
-     * @param clipEntry Platform specific clip object that either holds data or links to it.
+     * @param clipEntry Platform specific clip object that either holds data or links to it. Pass
+     * null to clear the clipboard.
      */
     @Suppress("GetterSetterNames")
-    fun setClip(clipEntry: ClipEntry) = Unit
+    fun setClip(clipEntry: ClipEntry?) = Unit
 
     /**
-     * Returns true if there is currently a primary clip on the platform Clipboard. Even though
-     * [getClip] should be available immediately, [getClipMetadata] may still return null if the
-     * platform doesn't support clip descriptions.
+     * Returns true if there is currently a clip entry on the platform Clipboard. Even though
+     * [getClip] should be available immediately if this function returns true, [getClipMetadata]
+     * may still return null if the platform doesn't support clip descriptions.
      */
     fun hasClip(): Boolean = false
 
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerEvent.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerEvent.desktop.kt
index 71519f8..b1cca07 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerEvent.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerEvent.desktop.kt
@@ -42,5 +42,5 @@
     actual var suppressMovementConsumption: Boolean = false
 
     // Assume that all changes are from mouse events for now
-    actual fun issuesEnterExitEvent(pointerId: PointerId): Boolean = true
+    actual fun activeHoverEvent(pointerId: PointerId): Boolean = true
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformClipboardManager.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformClipboardManager.desktop.kt
index b909150..30b1ee6 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformClipboardManager.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformClipboardManager.desktop.kt
@@ -24,7 +24,6 @@
 import java.awt.datatransfer.Transferable
 import java.awt.datatransfer.UnsupportedFlavorException
 import java.io.IOException
-import java.lang.IllegalStateException
 
 internal actual class PlatformClipboardManager : ClipboardManager {
     internal val systemClipboard = try {
@@ -63,9 +62,9 @@
         }
     }
 
-    override fun setClip(clipEntry: ClipEntry) {
+    override fun setClip(clipEntry: ClipEntry?) {
         // Ignore clipDescription.
-        systemClipboard?.setContents(clipEntry.transferable, null)
+        systemClipboard?.setContents(clipEntry?.transferable, null)
     }
 
     override fun hasClip(): Boolean {
diff --git a/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewStructureCompat.java b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewStructureCompat.java
index 1dd6bdb..1052324 100644
--- a/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewStructureCompat.java
+++ b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewStructureCompat.java
@@ -75,6 +75,27 @@
     }
 
     /**
+     * Set the identifier for this view.
+     *
+     * @param id The view's identifier, as per {@link android.view.View#getId View.getId()}.
+     * @param packageName The package name of the view's identifier, or null if there is none.
+     * @param typeName The type name of the view's identifier, or null if there is none.
+     * @param entryName The entry name of the view's identifier, or null if there is none.
+     *
+     * Compatibility behavior:
+     * <ul>
+     * <li>SDK 23 and above, this method matches platform behavior.
+     * <li>SDK 22 and below, this method does nothing.
+     * </ul>
+     */
+    public void setId(int id, @Nullable String packageName, @Nullable String typeName,
+            @Nullable String entryName) {
+        if (SDK_INT >= 23) {
+            Api23Impl.setId((ViewStructure) mWrappedObj, id, packageName, typeName, entryName);
+        }
+    }
+
+    /**
      * Set the text that is associated with this view.  There is no selection
      * associated with the text.  The text may have style spans to supply additional
      * display and semantic information.
@@ -194,6 +215,11 @@
             // This class is not instantiable.
         }
 
+        static void setId(ViewStructure viewStructure, int id, String packageName, String typeName,
+                String entryName) {
+            viewStructure.setId(id, packageName, typeName, entryName);
+        }
+
         @DoNotInline
         static void setDimens(ViewStructure viewStructure, int left, int top, int scrollX,
                 int scrollY, int width, int height) {
diff --git a/concurrent/concurrent-futures/api/restricted_current.txt b/concurrent/concurrent-futures/api/restricted_current.txt
index 9926fa1..32be46b 100644
--- a/concurrent/concurrent-futures/api/restricted_current.txt
+++ b/concurrent/concurrent-futures/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.concurrent.futures {
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class AbstractResolvableFuture<V> implements com.google.common.util.concurrent.ListenableFuture<V> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class AbstractResolvableFuture<V> implements com.google.common.util.concurrent.ListenableFuture<V!> {
     ctor protected AbstractResolvableFuture();
     method public final void addListener(Runnable!, java.util.concurrent.Executor!);
     method protected void afterDone();
@@ -34,7 +34,7 @@
     method public Object? attachCompleter(androidx.concurrent.futures.CallbackToFutureAdapter.Completer<T!>) throws java.lang.Exception;
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ResolvableFuture<V> extends androidx.concurrent.futures.AbstractResolvableFuture<V> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ResolvableFuture<V> extends androidx.concurrent.futures.AbstractResolvableFuture<V!> {
     method public static <V> androidx.concurrent.futures.ResolvableFuture<V!> create();
     method public boolean set(V?);
     method public boolean setException(Throwable);
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/build.gradle
index 488e08f..2f9c076 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/build.gradle
@@ -26,8 +26,6 @@
     }
     namespace "androidx.constraintlayout.compose.integration.macrobenchmark"
 
-    // We need animations to work for MotionLayout
-    testOptions.animationsDisabled  false
     targetProjectPath = ":constraintlayout:constraintlayout-compose:integration-tests:macrobenchmark-target"
     experimentalProperties["android.experimental.self-instrumenting"] = true
 }
diff --git a/constraintlayout/constraintlayout-core/api/current.txt b/constraintlayout/constraintlayout-core/api/current.txt
index 6d82925..184a753 100644
--- a/constraintlayout/constraintlayout-core/api/current.txt
+++ b/constraintlayout/constraintlayout-core/api/current.txt
@@ -182,7 +182,7 @@
     ctor public PriorityGoalRow(androidx.constraintlayout.core.Cache!);
   }
 
-  public class SolverVariable implements java.lang.Comparable<androidx.constraintlayout.core.SolverVariable> {
+  public class SolverVariable implements java.lang.Comparable<androidx.constraintlayout.core.SolverVariable!> {
     ctor public SolverVariable(androidx.constraintlayout.core.SolverVariable.Type!, String!);
     ctor public SolverVariable(String!, androidx.constraintlayout.core.SolverVariable.Type!);
     method public final void addToRow(androidx.constraintlayout.core.ArrayRow!);
@@ -954,7 +954,7 @@
     field public String! mId;
   }
 
-  public class MotionPaths implements java.lang.Comparable<androidx.constraintlayout.core.motion.MotionPaths> {
+  public class MotionPaths implements java.lang.Comparable<androidx.constraintlayout.core.motion.MotionPaths!> {
     ctor public MotionPaths();
     ctor public MotionPaths(int, int, androidx.constraintlayout.core.motion.key.MotionKeyPosition!, androidx.constraintlayout.core.motion.MotionPaths!, androidx.constraintlayout.core.motion.MotionPaths!);
     method public void applyParameters(androidx.constraintlayout.core.motion.MotionWidget!);
@@ -1953,7 +1953,7 @@
     method public void putValue(float);
   }
 
-  public class CLObject extends androidx.constraintlayout.core.parser.CLContainer implements java.lang.Iterable<androidx.constraintlayout.core.parser.CLKey> {
+  public class CLObject extends androidx.constraintlayout.core.parser.CLContainer implements java.lang.Iterable<androidx.constraintlayout.core.parser.CLKey!> {
     ctor public CLObject(char[]!);
     method public static androidx.constraintlayout.core.parser.CLObject! allocate(char[]!);
     method public androidx.constraintlayout.core.parser.CLObject clone();
diff --git a/constraintlayout/constraintlayout-core/api/restricted_current.txt b/constraintlayout/constraintlayout-core/api/restricted_current.txt
index 5975f5e..0b2ac8fe 100644
--- a/constraintlayout/constraintlayout-core/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-core/api/restricted_current.txt
@@ -182,7 +182,7 @@
     ctor public PriorityGoalRow(androidx.constraintlayout.core.Cache!);
   }
 
-  public class SolverVariable implements java.lang.Comparable<androidx.constraintlayout.core.SolverVariable> {
+  public class SolverVariable implements java.lang.Comparable<androidx.constraintlayout.core.SolverVariable!> {
     ctor public SolverVariable(androidx.constraintlayout.core.SolverVariable.Type!, String!);
     ctor public SolverVariable(String!, androidx.constraintlayout.core.SolverVariable.Type!);
     method public final void addToRow(androidx.constraintlayout.core.ArrayRow!);
@@ -954,7 +954,7 @@
     field public String! mId;
   }
 
-  public class MotionPaths implements java.lang.Comparable<androidx.constraintlayout.core.motion.MotionPaths> {
+  public class MotionPaths implements java.lang.Comparable<androidx.constraintlayout.core.motion.MotionPaths!> {
     ctor public MotionPaths();
     ctor public MotionPaths(int, int, androidx.constraintlayout.core.motion.key.MotionKeyPosition!, androidx.constraintlayout.core.motion.MotionPaths!, androidx.constraintlayout.core.motion.MotionPaths!);
     method public void applyParameters(androidx.constraintlayout.core.motion.MotionWidget!);
@@ -1953,7 +1953,7 @@
     method public void putValue(float);
   }
 
-  public class CLObject extends androidx.constraintlayout.core.parser.CLContainer implements java.lang.Iterable<androidx.constraintlayout.core.parser.CLKey> {
+  public class CLObject extends androidx.constraintlayout.core.parser.CLContainer implements java.lang.Iterable<androidx.constraintlayout.core.parser.CLKey!> {
     ctor public CLObject(char[]!);
     method public static androidx.constraintlayout.core.parser.CLObject! allocate(char[]!);
     method public androidx.constraintlayout.core.parser.CLObject clone();
diff --git a/core/core-animation/api/current.txt b/core/core-animation/api/current.txt
index 71e774e..54cc9b1 100644
--- a/core/core-animation/api/current.txt
+++ b/core/core-animation/api/current.txt
@@ -122,12 +122,12 @@
     method @FloatRange(to=1) public float getInterpolation(@FloatRange(from=0, to=1) float);
   }
 
-  public final class ArgbEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Integer> {
+  public final class ArgbEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Integer!> {
     method public Integer evaluate(float, Integer, Integer);
     method public static androidx.core.animation.ArgbEvaluator getInstance();
   }
 
-  public abstract class BidirectionalTypeConverter<T, V> extends androidx.core.animation.TypeConverter<T,V> {
+  public abstract class BidirectionalTypeConverter<T, V> extends androidx.core.animation.TypeConverter<T!,V!> {
     ctor public BidirectionalTypeConverter(Class<T!>, Class<V!>);
     method public abstract T convertBack(V);
     method public androidx.core.animation.BidirectionalTypeConverter<V!,T!> invert();
@@ -151,36 +151,36 @@
     method @FloatRange(from=0, to=1) public float getInterpolation(@FloatRange(from=0, to=1) float);
   }
 
-  public final class FloatArrayEvaluator implements androidx.core.animation.TypeEvaluator<float[]> {
+  public final class FloatArrayEvaluator implements androidx.core.animation.TypeEvaluator<float[]!> {
     ctor public FloatArrayEvaluator();
     ctor public FloatArrayEvaluator(float[]?);
     method public float[] evaluate(float, float[], float[]);
   }
 
-  public final class FloatEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Float> {
+  public final class FloatEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Float!> {
     method public Float evaluate(float, Float, Float);
     method public static androidx.core.animation.FloatEvaluator getInstance();
   }
 
-  public abstract class FloatProperty<T> extends android.util.Property<T,java.lang.Float> {
+  public abstract class FloatProperty<T> extends android.util.Property<T!,java.lang.Float!> {
     ctor public FloatProperty();
     ctor public FloatProperty(String);
     method public final void set(T, Float);
     method public abstract void setValue(T, float);
   }
 
-  public class IntArrayEvaluator implements androidx.core.animation.TypeEvaluator<int[]> {
+  public class IntArrayEvaluator implements androidx.core.animation.TypeEvaluator<int[]!> {
     ctor public IntArrayEvaluator();
     ctor public IntArrayEvaluator(int[]?);
     method public int[] evaluate(float, int[], int[]);
   }
 
-  public class IntEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Integer> {
+  public class IntEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Integer!> {
     method public Integer evaluate(float, Integer, Integer);
     method public static androidx.core.animation.IntEvaluator getInstance();
   }
 
-  public abstract class IntProperty<T> extends android.util.Property<T,java.lang.Integer> {
+  public abstract class IntProperty<T> extends android.util.Property<T!,java.lang.Integer!> {
     ctor public IntProperty();
     ctor public IntProperty(String);
     method public final void set(T, Integer);
@@ -265,7 +265,7 @@
     method public float getInterpolation(@FloatRange(from=0, to=1) float);
   }
 
-  public class PointFEvaluator implements androidx.core.animation.TypeEvaluator<android.graphics.PointF> {
+  public class PointFEvaluator implements androidx.core.animation.TypeEvaluator<android.graphics.PointF!> {
     ctor public PointFEvaluator();
     ctor public PointFEvaluator(android.graphics.PointF);
     method public android.graphics.PointF evaluate(float, android.graphics.PointF, android.graphics.PointF);
@@ -303,7 +303,7 @@
     method public void setPropertyName(String);
   }
 
-  public class RectEvaluator implements androidx.core.animation.TypeEvaluator<android.graphics.Rect> {
+  public class RectEvaluator implements androidx.core.animation.TypeEvaluator<android.graphics.Rect!> {
     ctor public RectEvaluator();
     ctor public RectEvaluator(android.graphics.Rect);
     method public android.graphics.Rect evaluate(float, android.graphics.Rect, android.graphics.Rect);
diff --git a/core/core-animation/api/restricted_current.txt b/core/core-animation/api/restricted_current.txt
index 71e774e..54cc9b1 100644
--- a/core/core-animation/api/restricted_current.txt
+++ b/core/core-animation/api/restricted_current.txt
@@ -122,12 +122,12 @@
     method @FloatRange(to=1) public float getInterpolation(@FloatRange(from=0, to=1) float);
   }
 
-  public final class ArgbEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Integer> {
+  public final class ArgbEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Integer!> {
     method public Integer evaluate(float, Integer, Integer);
     method public static androidx.core.animation.ArgbEvaluator getInstance();
   }
 
-  public abstract class BidirectionalTypeConverter<T, V> extends androidx.core.animation.TypeConverter<T,V> {
+  public abstract class BidirectionalTypeConverter<T, V> extends androidx.core.animation.TypeConverter<T!,V!> {
     ctor public BidirectionalTypeConverter(Class<T!>, Class<V!>);
     method public abstract T convertBack(V);
     method public androidx.core.animation.BidirectionalTypeConverter<V!,T!> invert();
@@ -151,36 +151,36 @@
     method @FloatRange(from=0, to=1) public float getInterpolation(@FloatRange(from=0, to=1) float);
   }
 
-  public final class FloatArrayEvaluator implements androidx.core.animation.TypeEvaluator<float[]> {
+  public final class FloatArrayEvaluator implements androidx.core.animation.TypeEvaluator<float[]!> {
     ctor public FloatArrayEvaluator();
     ctor public FloatArrayEvaluator(float[]?);
     method public float[] evaluate(float, float[], float[]);
   }
 
-  public final class FloatEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Float> {
+  public final class FloatEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Float!> {
     method public Float evaluate(float, Float, Float);
     method public static androidx.core.animation.FloatEvaluator getInstance();
   }
 
-  public abstract class FloatProperty<T> extends android.util.Property<T,java.lang.Float> {
+  public abstract class FloatProperty<T> extends android.util.Property<T!,java.lang.Float!> {
     ctor public FloatProperty();
     ctor public FloatProperty(String);
     method public final void set(T, Float);
     method public abstract void setValue(T, float);
   }
 
-  public class IntArrayEvaluator implements androidx.core.animation.TypeEvaluator<int[]> {
+  public class IntArrayEvaluator implements androidx.core.animation.TypeEvaluator<int[]!> {
     ctor public IntArrayEvaluator();
     ctor public IntArrayEvaluator(int[]?);
     method public int[] evaluate(float, int[], int[]);
   }
 
-  public class IntEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Integer> {
+  public class IntEvaluator implements androidx.core.animation.TypeEvaluator<java.lang.Integer!> {
     method public Integer evaluate(float, Integer, Integer);
     method public static androidx.core.animation.IntEvaluator getInstance();
   }
 
-  public abstract class IntProperty<T> extends android.util.Property<T,java.lang.Integer> {
+  public abstract class IntProperty<T> extends android.util.Property<T!,java.lang.Integer!> {
     ctor public IntProperty();
     ctor public IntProperty(String);
     method public final void set(T, Integer);
@@ -265,7 +265,7 @@
     method public float getInterpolation(@FloatRange(from=0, to=1) float);
   }
 
-  public class PointFEvaluator implements androidx.core.animation.TypeEvaluator<android.graphics.PointF> {
+  public class PointFEvaluator implements androidx.core.animation.TypeEvaluator<android.graphics.PointF!> {
     ctor public PointFEvaluator();
     ctor public PointFEvaluator(android.graphics.PointF);
     method public android.graphics.PointF evaluate(float, android.graphics.PointF, android.graphics.PointF);
@@ -303,7 +303,7 @@
     method public void setPropertyName(String);
   }
 
-  public class RectEvaluator implements androidx.core.animation.TypeEvaluator<android.graphics.Rect> {
+  public class RectEvaluator implements androidx.core.animation.TypeEvaluator<android.graphics.Rect!> {
     ctor public RectEvaluator();
     ctor public RectEvaluator(android.graphics.Rect);
     method public android.graphics.Rect evaluate(float, android.graphics.Rect, android.graphics.Rect);
diff --git a/core/core/api/1.13.0-beta01.txt b/core/core/api/1.13.0-beta01.txt
index f0bc2c9..57c66b5 100644
--- a/core/core/api/1.13.0-beta01.txt
+++ b/core/core/api/1.13.0-beta01.txt
@@ -1060,7 +1060,7 @@
     method public void onSharedElementsReady();
   }
 
-  public final class TaskStackBuilder implements java.lang.Iterable<android.content.Intent> {
+  public final class TaskStackBuilder implements java.lang.Iterable<android.content.Intent!> {
     method public androidx.core.app.TaskStackBuilder addNextIntent(android.content.Intent);
     method public androidx.core.app.TaskStackBuilder addNextIntentWithParentStack(android.content.Intent);
     method public androidx.core.app.TaskStackBuilder addParentStack(android.app.Activity);
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index f0bc2c9..57c66b5 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1060,7 +1060,7 @@
     method public void onSharedElementsReady();
   }
 
-  public final class TaskStackBuilder implements java.lang.Iterable<android.content.Intent> {
+  public final class TaskStackBuilder implements java.lang.Iterable<android.content.Intent!> {
     method public androidx.core.app.TaskStackBuilder addNextIntent(android.content.Intent);
     method public androidx.core.app.TaskStackBuilder addNextIntentWithParentStack(android.content.Intent);
     method public androidx.core.app.TaskStackBuilder addParentStack(android.app.Activity);
diff --git a/core/core/api/restricted_1.13.0-beta01.txt b/core/core/api/restricted_1.13.0-beta01.txt
index a226921..b090f80 100644
--- a/core/core/api/restricted_1.13.0-beta01.txt
+++ b/core/core/api/restricted_1.13.0-beta01.txt
@@ -1183,7 +1183,7 @@
     method public void onSharedElementsReady();
   }
 
-  public final class TaskStackBuilder implements java.lang.Iterable<android.content.Intent> {
+  public final class TaskStackBuilder implements java.lang.Iterable<android.content.Intent!> {
     method public androidx.core.app.TaskStackBuilder addNextIntent(android.content.Intent);
     method public androidx.core.app.TaskStackBuilder addNextIntentWithParentStack(android.content.Intent);
     method public androidx.core.app.TaskStackBuilder addParentStack(android.app.Activity);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index a226921..b090f80 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1183,7 +1183,7 @@
     method public void onSharedElementsReady();
   }
 
-  public final class TaskStackBuilder implements java.lang.Iterable<android.content.Intent> {
+  public final class TaskStackBuilder implements java.lang.Iterable<android.content.Intent!> {
     method public androidx.core.app.TaskStackBuilder addNextIntent(android.content.Intent);
     method public androidx.core.app.TaskStackBuilder addNextIntentWithParentStack(android.content.Intent);
     method public androidx.core.app.TaskStackBuilder addParentStack(android.app.Activity);
diff --git a/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java b/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
index e8e1bbf3..244b776 100644
--- a/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
@@ -18,16 +18,11 @@
 
 import android.Manifest;
 import android.content.Context;
-import android.content.pm.PackageManager;
-import android.hardware.fingerprint.FingerprintManager;
-import android.os.Build;
 import android.os.CancellationSignal;
 import android.os.Handler;
 
-import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
 
@@ -39,62 +34,53 @@
 /**
  * A class that coordinates access to the fingerprint hardware.
  * <p>
- * On platforms before {@link android.os.Build.VERSION_CODES#M}, this class behaves as there would
- * be no fingerprint hardware available.
+ * This class has been deprecated and should no longer be used. On all platform versions, it behaves
+ * as though no fingerprint hardware is available.
  *
- * @deprecated Use {@code androidx.biometrics.BiometricPrompt} instead.
+ * @deprecated {@code FingerprintManager} was removed from the platform SDK in Android V, use
+ * {@code androidx.biometrics.BiometricPrompt} instead.
  */
 @SuppressWarnings({"deprecation", "unused"})
 @Deprecated
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 public class FingerprintManagerCompat {
 
-    private final Context mContext;
-
     /** Get a {@link FingerprintManagerCompat} instance for a provided context. */
     @NonNull
     public static FingerprintManagerCompat from(@NonNull Context context) {
-        return new FingerprintManagerCompat(context);
+        return new FingerprintManagerCompat();
     }
 
-    private FingerprintManagerCompat(Context context) {
-        mContext = context;
+    private FingerprintManagerCompat() {
     }
 
     /**
-     * Determine if there is at least one fingerprint enrolled.
+     * Prior to deprecation, this method would determine if there is at least one fingerprint
+     * enrolled.
      *
-     * @return true if at least one fingerprint is enrolled, false otherwise
+     * @return false
      */
     @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
     public boolean hasEnrolledFingerprints() {
-        if (Build.VERSION.SDK_INT >= 23) {
-            final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
-            return (fp != null) && Api23Impl.hasEnrolledFingerprints(fp);
-        } else {
-            return false;
-        }
+        return false;
     }
 
     /**
-     * Determine if fingerprint hardware is present and functional.
+     * Prior to deprecation, this method would determine if fingerprint hardware is present and
+     * functional.
      *
-     * @return true if hardware is present and functional, false otherwise.
+     * @return false
      */
     @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
     public boolean isHardwareDetected() {
-        if (Build.VERSION.SDK_INT >= 23) {
-            final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
-            return (fp != null) && Api23Impl.isHardwareDetected(fp);
-        } else {
-            return false;
-        }
+        return false;
     }
 
     /**
-     * Request authentication of a crypto object. This call warms up the fingerprint hardware
-     * and starts scanning for a fingerprint. It terminates when
-     * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
+     * Prior to deprecation, this method would request authentication of a crypto object.
+     * <p>
+     * This call warms up the fingerprint hardware and starts scanning for a fingerprint. It
+     * terminates when {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
      * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
      * which point the object is no longer valid. The operation can be canceled by using the
      * provided cancel object.
@@ -114,15 +100,14 @@
             @Nullable androidx.core.os.CancellationSignal cancel,
             @NonNull AuthenticationCallback callback,
             @Nullable Handler handler) {
-        authenticate(crypto, flags,
-                cancel != null ? (CancellationSignal) cancel.getCancellationSignalObject() : null,
-                callback, handler);
+        // No-op.
     }
 
     /**
-     * Request authentication of a crypto object. This call warms up the fingerprint hardware
-     * and starts scanning for a fingerprint. It terminates when
-     * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
+     * Prior to deprecation, this method would request authentication of a crypto object.
+     * <p>
+     * This call warms up the fingerprint hardware and starts scanning for a fingerprint.
+     * It terminates when {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
      * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
      * which point the object is no longer valid. The operation can be canceled by using the
      * provided cancel object.
@@ -137,56 +122,7 @@
     public void authenticate(@Nullable CryptoObject crypto, int flags,
             @Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback,
             @Nullable Handler handler) {
-        if (Build.VERSION.SDK_INT >= 23) {
-            final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
-            if (fp != null) {
-                Api23Impl.authenticate(fp, wrapCryptoObject(crypto), cancel, flags,
-                        wrapCallback(callback), handler);
-            }
-        }
-    }
-
-    @Nullable
-    @RequiresApi(23)
-    private static FingerprintManager getFingerprintManagerOrNull(@NonNull Context context) {
-        return Api23Impl.getFingerprintManagerOrNull(context);
-    }
-
-    @RequiresApi(23)
-    private static FingerprintManager.CryptoObject wrapCryptoObject(CryptoObject cryptoObject) {
-        return Api23Impl.wrapCryptoObject(cryptoObject);
-    }
-
-    @RequiresApi(23)
-    static CryptoObject unwrapCryptoObject(FingerprintManager.CryptoObject cryptoObject) {
-        return Api23Impl.unwrapCryptoObject(cryptoObject);
-    }
-
-    @RequiresApi(23)
-    private static FingerprintManager.AuthenticationCallback wrapCallback(
-            final AuthenticationCallback callback) {
-        return new FingerprintManager.AuthenticationCallback() {
-            @Override
-            public void onAuthenticationError(int errMsgId, CharSequence errString) {
-                callback.onAuthenticationError(errMsgId, errString);
-            }
-
-            @Override
-            public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
-                callback.onAuthenticationHelp(helpMsgId, helpString);
-            }
-
-            @Override
-            public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
-                callback.onAuthenticationSucceeded(new AuthenticationResult(
-                        unwrapCryptoObject(Api23Impl.getCryptoObject(result))));
-            }
-
-            @Override
-            public void onAuthenticationFailed() {
-                callback.onAuthenticationFailed();
-            }
-        };
+        // No-op.
     }
 
     /**
@@ -296,82 +232,4 @@
          */
         public void onAuthenticationFailed() { }
     }
-
-    @RequiresApi(23)
-    static class Api23Impl {
-        private Api23Impl() {
-            // This class is not instantiable.
-        }
-
-        @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
-        @DoNotInline
-        static boolean hasEnrolledFingerprints(Object fingerprintManager) {
-            return ((FingerprintManager) fingerprintManager).hasEnrolledFingerprints();
-        }
-
-        @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
-        @DoNotInline
-        static boolean isHardwareDetected(Object fingerprintManager) {
-            return ((FingerprintManager) fingerprintManager).isHardwareDetected();
-        }
-
-        @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
-        @DoNotInline
-        static void authenticate(Object fingerprintManager, Object crypto,
-                CancellationSignal cancel, int flags, Object callback, Handler handler) {
-            ((FingerprintManager) fingerprintManager).authenticate(
-                    (FingerprintManager.CryptoObject) crypto, cancel, flags,
-                    (FingerprintManager.AuthenticationCallback) callback, handler);
-        }
-
-        @DoNotInline
-        static FingerprintManager.CryptoObject getCryptoObject(Object authenticationResult) {
-            return ((FingerprintManager.AuthenticationResult) authenticationResult)
-                    .getCryptoObject();
-        }
-
-        @DoNotInline
-        public static FingerprintManager getFingerprintManagerOrNull(Context context) {
-            if (Build.VERSION.SDK_INT == 23) {
-                return context.getSystemService(FingerprintManager.class);
-            } else if (Build.VERSION.SDK_INT > 23 && context.getPackageManager()
-                    .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
-                return context.getSystemService(FingerprintManager.class);
-            } else {
-                return null;
-            }
-        }
-
-        @DoNotInline
-        public static FingerprintManager.CryptoObject wrapCryptoObject(CryptoObject cryptoObject) {
-            if (cryptoObject == null) {
-                return null;
-            } else if (cryptoObject.getCipher() != null) {
-                return new FingerprintManager.CryptoObject(cryptoObject.getCipher());
-            } else if (cryptoObject.getSignature() != null) {
-                return new FingerprintManager.CryptoObject(cryptoObject.getSignature());
-            } else if (cryptoObject.getMac() != null) {
-                return new FingerprintManager.CryptoObject(cryptoObject.getMac());
-            } else {
-                return null;
-            }
-        }
-
-        @DoNotInline
-        public static CryptoObject unwrapCryptoObject(Object cryptoObjectObj) {
-            FingerprintManager.CryptoObject cryptoObject =
-                    (FingerprintManager.CryptoObject) cryptoObjectObj;
-            if (cryptoObject == null) {
-                return null;
-            } else if (cryptoObject.getCipher() != null) {
-                return new CryptoObject(cryptoObject.getCipher());
-            } else if (cryptoObject.getSignature() != null) {
-                return new CryptoObject(cryptoObject.getSignature());
-            } else if (cryptoObject.getMac() != null) {
-                return new CryptoObject(cryptoObject.getMac());
-            } else {
-                return null;
-            }
-        }
-    }
 }
diff --git a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
index 30f06ef..3d3edff 100644
--- a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
@@ -26,7 +26,6 @@
 import kotlin.contracts.ExperimentalContracts
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
@@ -37,9 +36,6 @@
 ) : InterProcessCoordinator {
     // TODO(b/269375542): the flow should `flowOn` the provided [context]
     override val updateNotifications: Flow<Unit> = MulticastFileObserver.observe(file)
-        // MulticastFileObserver dispatches 1 value upon connecting to the FileSystem, which
-        // is useful for its tests but not necessary here.
-        .drop(1)
 
     // run block with the exclusive lock
     override suspend fun <T> lock(block: suspend () -> T): T {
diff --git a/development/bench-flame-diff/README.md b/development/bench-flame-diff/README.md
index 87977c0..cfc9889b 100644
--- a/development/bench-flame-diff/README.md
+++ b/development/bench-flame-diff/README.md
@@ -56,7 +56,9 @@
 
 ## CLI completion
 
-Generate completion files with `./generate-completion-files.sh` and source in your shell config, e.g.:
+Generate shell-specific completion files with `./generate-completion.sh`.
+
+Then, source in your shell config, e.g.:
 - For `bash`: `dst="$(pwd)/completion_bash.sh"; echo "source '$dst'" >> ~/.bashrc`
 - For `zsh`: `dst="$(pwd)/completion_zsh.sh"; echo "source '$dst'" >> ~/.zshrc`
 
diff --git a/dynamicanimation/dynamicanimation/api/current.txt b/dynamicanimation/dynamicanimation/api/current.txt
index 1efc224..c23948c 100644
--- a/dynamicanimation/dynamicanimation/api/current.txt
+++ b/dynamicanimation/dynamicanimation/api/current.txt
@@ -51,10 +51,10 @@
     method public void onAnimationUpdate(androidx.dynamicanimation.animation.DynamicAnimation!, float, float);
   }
 
-  public abstract static class DynamicAnimation.ViewProperty extends androidx.dynamicanimation.animation.FloatPropertyCompat<android.view.View> {
+  public abstract static class DynamicAnimation.ViewProperty extends androidx.dynamicanimation.animation.FloatPropertyCompat<android.view.View!> {
   }
 
-  public final class FlingAnimation extends androidx.dynamicanimation.animation.DynamicAnimation<androidx.dynamicanimation.animation.FlingAnimation> {
+  public final class FlingAnimation extends androidx.dynamicanimation.animation.DynamicAnimation<androidx.dynamicanimation.animation.FlingAnimation!> {
     ctor public FlingAnimation(androidx.dynamicanimation.animation.FloatValueHolder!);
     ctor public <K> FlingAnimation(K!, androidx.dynamicanimation.animation.FloatPropertyCompat<K!>!);
     method public float getFriction();
@@ -83,7 +83,7 @@
     method public void postFrameCallback(Runnable);
   }
 
-  public final class SpringAnimation extends androidx.dynamicanimation.animation.DynamicAnimation<androidx.dynamicanimation.animation.SpringAnimation> {
+  public final class SpringAnimation extends androidx.dynamicanimation.animation.DynamicAnimation<androidx.dynamicanimation.animation.SpringAnimation!> {
     ctor public SpringAnimation(androidx.dynamicanimation.animation.FloatValueHolder!);
     ctor public SpringAnimation(androidx.dynamicanimation.animation.FloatValueHolder!, float);
     ctor public <K> SpringAnimation(K!, androidx.dynamicanimation.animation.FloatPropertyCompat<K!>!);
diff --git a/dynamicanimation/dynamicanimation/api/restricted_current.txt b/dynamicanimation/dynamicanimation/api/restricted_current.txt
index 1efc224..c23948c 100644
--- a/dynamicanimation/dynamicanimation/api/restricted_current.txt
+++ b/dynamicanimation/dynamicanimation/api/restricted_current.txt
@@ -51,10 +51,10 @@
     method public void onAnimationUpdate(androidx.dynamicanimation.animation.DynamicAnimation!, float, float);
   }
 
-  public abstract static class DynamicAnimation.ViewProperty extends androidx.dynamicanimation.animation.FloatPropertyCompat<android.view.View> {
+  public abstract static class DynamicAnimation.ViewProperty extends androidx.dynamicanimation.animation.FloatPropertyCompat<android.view.View!> {
   }
 
-  public final class FlingAnimation extends androidx.dynamicanimation.animation.DynamicAnimation<androidx.dynamicanimation.animation.FlingAnimation> {
+  public final class FlingAnimation extends androidx.dynamicanimation.animation.DynamicAnimation<androidx.dynamicanimation.animation.FlingAnimation!> {
     ctor public FlingAnimation(androidx.dynamicanimation.animation.FloatValueHolder!);
     ctor public <K> FlingAnimation(K!, androidx.dynamicanimation.animation.FloatPropertyCompat<K!>!);
     method public float getFriction();
@@ -83,7 +83,7 @@
     method public void postFrameCallback(Runnable);
   }
 
-  public final class SpringAnimation extends androidx.dynamicanimation.animation.DynamicAnimation<androidx.dynamicanimation.animation.SpringAnimation> {
+  public final class SpringAnimation extends androidx.dynamicanimation.animation.DynamicAnimation<androidx.dynamicanimation.animation.SpringAnimation!> {
     ctor public SpringAnimation(androidx.dynamicanimation.animation.FloatValueHolder!);
     ctor public SpringAnimation(androidx.dynamicanimation.animation.FloatValueHolder!, float);
     ctor public <K> SpringAnimation(K!, androidx.dynamicanimation.animation.FloatPropertyCompat<K!>!);
diff --git a/emoji2/emoji2/api/current.txt b/emoji2/emoji2/api/current.txt
index a81c4c0..494e7a5 100644
--- a/emoji2/emoji2/api/current.txt
+++ b/emoji2/emoji2/api/current.txt
@@ -84,7 +84,7 @@
     method public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
   }
 
-  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean!> {
     ctor public EmojiCompatInitializer();
     method public Boolean create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>!>!> dependencies();
diff --git a/emoji2/emoji2/api/restricted_current.txt b/emoji2/emoji2/api/restricted_current.txt
index a81c4c0..494e7a5 100644
--- a/emoji2/emoji2/api/restricted_current.txt
+++ b/emoji2/emoji2/api/restricted_current.txt
@@ -84,7 +84,7 @@
     method public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
   }
 
-  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean!> {
     ctor public EmojiCompatInitializer();
     method public Boolean create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>!>!> dependencies();
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetector.kt
index 817bdc4..3c27b95 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetector.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetector.kt
@@ -74,14 +74,14 @@
 
     private inner class UastHandler(val context: JavaContext) : UElementHandler() {
         override fun visitClass(node: UClass) {
-            if (isKotlin(context.psiFile) &&
+            if (isKotlin(node.lang) &&
                 (node.sourcePsi as? KtClassOrObject)?.getSuperNames()?.firstOrNull() !=
                 DIALOG_FRAGMENT_CLASS
             ) {
                 return
             }
 
-            if (!isKotlin(context.psiFile) &&
+            if (!isKotlin(node.lang) &&
                 (node.uastSuperTypes.firstOrNull()?.type as? PsiClassReferenceType)
                     ?.className != DIALOG_FRAGMENT_CLASS
             ) {
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt
index 8ed43ac..b07db9f 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt
@@ -178,7 +178,7 @@
             ) {
                 val argType = PsiTypesUtil.getPsiClass(arg.getExpressionType())
                 if (argType == call.getContainingUClass()?.javaPsi) {
-                    val methodFix = if (isKotlin(context.psiFile)) {
+                    val methodFix = if (isKotlin(call.lang)) {
                         "viewLifecycleOwner"
                     } else {
                         "getViewLifecycleOwner()"
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseGetLayoutInflater.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseGetLayoutInflater.kt
index ffd14a5..853bac5 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseGetLayoutInflater.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseGetLayoutInflater.kt
@@ -31,6 +31,7 @@
 import com.intellij.psi.PsiMethod
 import com.intellij.psi.PsiType
 import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
 import org.jetbrains.uast.getContainingUClass
 
 /**
@@ -85,8 +86,8 @@
             issue = ISSUE,
             location = context.getLocation(node),
             message = "Use of LayoutInflater.from($methodParameter) detected. Consider using " +
-                "${correctMethod(context)} instead",
-            quickfixData = createFix(correctMethod(context), methodParameter)
+                "${correctMethod(node)} instead",
+            quickfixData = createFix(correctMethod(node), methodParameter)
         )
     }
 
@@ -105,13 +106,13 @@
             issue = ISSUE,
             location = context.getLocation(node),
             message = "Use of LayoutInflater.from(Context) detected. Consider using " +
-                "${correctMethod(context)} instead",
+                "${correctMethod(node)} instead",
             quickfixData = null
         )
     }
 
-    private fun correctMethod(context: JavaContext): String {
-        return if (isKotlin(context.psiFile)) {
+    private fun correctMethod(context: UElement): String {
+        return if (isKotlin(context.lang)) {
             "layoutInflater"
         } else {
             "getLayoutInflater()"
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
index 193833b..82e4caf 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
@@ -99,7 +99,6 @@
     }
 
     override fun createUastHandler(context: JavaContext): UElementHandler? {
-        val isKotlin = isKotlin(context.psiFile)
         return object : UElementHandler() {
 
             /** This covers Kotlin accessor syntax expressions like "fragment.arguments" */
@@ -159,7 +158,7 @@
                 // Note we go up potentially two parents - the first one may just be the qualified reference expression
                 val nearestNonQualifiedReferenceParent =
                     skipParenthesizedExprUp(node.nearestNonQualifiedReferenceParent) ?: return
-                if (isKotlin && nearestNonQualifiedReferenceParent.isNullCheckBlock()) {
+                if (isKotlin(node.lang) && nearestNonQualifiedReferenceParent.isNullCheckBlock()) {
                     // We're a double-bang expression (!!)
                     val parentSourceToReplace =
                         nearestNonQualifiedReferenceParent.asSourceString()
@@ -216,7 +215,7 @@
                 nearestNonQualifiedRefParent: UCallExpression
             ) = enclosingMethodCall.parameterList.parametersCount == 1 ||
                 (
-                    isKotlin &&
+                    isKotlin(nearestNonQualifiedRefParent.lang) &&
                         nearestNonQualifiedRefParent.getArgumentForParameter(1) == null
                     )
 
diff --git a/gradle.properties b/gradle.properties
index 1d887ab..0204323 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -44,6 +44,9 @@
 androidx.allowCustomCompileSdk=true
 androidx.includeOptionalProjects=false
 
+# Keep ComposeCompiler pinned unless performing Kotlin upgrade & ComposeCompiler release
+androidx.unpinComposeCompiler=true
+
 # Disable features we do not use
 android.defaults.buildfeatures.aidl=false
 android.defaults.buildfeatures.buildconfig=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a13359f..5bc5bee 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -41,10 +41,10 @@
 jcodec = "0.2.5"
 kotlin17 = "1.7.10"
 kotlin18 = "1.8.22"
-kotlin19 = "1.9.22"
-kotlin = "1.9.22"
+kotlin19 = "1.9.23"
+kotlin = "1.9.23"
 kotlinBenchmark = "0.4.8"
-kotlinNative = "1.9.22"
+kotlinNative = "1.9.23"
 kotlinCompileTesting = "1.4.9"
 kotlinCoroutines = "1.7.3"
 kotlinSerialization = "1.6.3"
@@ -188,6 +188,7 @@
 kotlinMetadataJvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.9.0" }
 kotlinSerializationCore = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinSerialization" }
 kotlinSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" }
+kotlinSerializationJsonOkio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinSerialization" }
 kotlinSerializationProtobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinSerialization" }
 kotlinGradlePluginz = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
 kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 43fade8..a85faf4 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -20,9 +20,9 @@
          <trust group="com.google.mlkit" reason="b/223907608"/>
          <trust group="com.google.testing.platform" reason="b/215430394"/>
          <trust group="gradle" name="gradle"/>
-         <trust file=".*[.]asc" regex="true"/>
          <trust file=".*-javadoc[.]jar" regex="true"/>
          <trust file=".*-sources[.]jar" regex="true"/>
+         <trust file=".*[.]asc" regex="true"/>
          <trust group="^androidx(?!\.compose.compiler\b)\..*" regex="true" reason="not signed yet"/>
          <trust group="^com[.]android($|([.].*))" regex="true" reason="b/215430394"/>
       </trusted-artifacts>
@@ -488,19 +488,19 @@
       </trusted-keys>
    </configuration>
    <components>
-      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.9.22">
-         <artifact name="kotlin-native-prebuilt-linux-x86_64-1.9.22.tar.gz">
-            <sha256 value="4653979dd53623b92439def2ea28706c2842eaae84bdfa6ba7703dad210a1328" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.9.22.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
+      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.9.23">
+         <artifact name="kotlin-native-prebuilt-linux-x86_64-1.9.23.tar.gz">
+            <sha256 value="ffab3a3c870bbc99770ce3971e04823cc7a7d215fee0eb496ea4af1df7e90e19" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.9.23.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
          </artifact>
       </component>
-      <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.9.22">
-         <artifact name="kotlin-native-prebuilt-macos-aarch64-1.9.22.tar.gz">
-            <sha256 value="20f4ff427763c19fe20ffaae0b0447eb8ebb17d2f834ca9208c7a22bdc5ffbc0" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.9.22.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
+      <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.9.23">
+         <artifact name="kotlin-native-prebuilt-macos-aarch64-1.9.23.tar.gz">
+            <sha256 value="9a3a9e94ac63b66477896b79a398a5dd84517054c848191edb18954443428b5a" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.9.23.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
          </artifact>
       </component>
-      <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.9.22">
-         <artifact name="kotlin-native-prebuilt-macos-x86_64-1.9.22.tar.gz">
-            <sha256 value="abe5f19ac2a9df76a3e8822d54447f0322252f3620da5394ab728beea836ec9e" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.9.22.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
+      <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.9.23">
+         <artifact name="kotlin-native-prebuilt-macos-x86_64-1.9.23.tar.gz">
+            <sha256 value="dd50c2e01db9e7232eed72c8de391a5016e8ef8edab4f5fdbd679034852e72b5" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.9.23.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
          </artifact>
       </component>
       <component group="aopalliance" name="aopalliance" version="1.0">
@@ -784,6 +784,14 @@
             <pgp value="720746177725A89207A7075BFD5DEA07FCB690A8"/>
          </artifact>
       </component>
+      <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin" version="1.9.23">
+         <artifact name="kotlin-gradle-plugin-1.9.23-gradle82.jar">
+            <ignored-keys>
+               <ignored-key id="6F538074CCEBF35F28AF9B066A0975F8B1127B83" reason="Temporary diagnostic for b/321949384"/>
+            </ignored-keys>
+            <sha256 value="9cc4a2206fa5f1adbb4e416f267dc22d8207905b8a1e138c02cb3fac24c67e14" origin="Generated by Gradle" reason="Temporary diagnostic for b/321949384"/>
+         </artifact>
+      </component>
       <component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.3.71">
          <artifact name="kotlin-reflect-1.3.71.pom">
             <sha256 value="4df94aaeee8d900be431386e31ef44e82a66e57c3ae30866aec2875aff01fe70" origin="Generated by Gradle"/>
diff --git a/graphics/graphics-path/build.gradle b/graphics/graphics-path/build.gradle
index 091c671..753e958 100644
--- a/graphics/graphics-path/build.gradle
+++ b/graphics/graphics-path/build.gradle
@@ -67,6 +67,7 @@
                     "-fomit-frame-pointer",
                     "-ffunction-sections",
                     "-fdata-sections",
+                    "-fstack-protector",
                     "-Wl,--gc-sections",
                     "-Wl,-Bsymbolic-functions",
                     "-nostdlib++"
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 65e848f..4a99988 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -117,7 +117,7 @@
 
 package androidx.health.connect.client.contracts {
 
-  public final class ExerciseRouteRequestContract extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,androidx.health.connect.client.records.ExerciseRoute> {
+  public final class ExerciseRouteRequestContract extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,androidx.health.connect.client.records.ExerciseRoute?> {
     ctor public ExerciseRouteRequestContract();
     method public android.content.Intent createIntent(android.content.Context context, String input);
     method public androidx.health.connect.client.records.ExerciseRoute? parseResult(int resultCode, android.content.Intent? intent);
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index f249592..8eb6800 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -117,7 +117,7 @@
 
 package androidx.health.connect.client.contracts {
 
-  public final class ExerciseRouteRequestContract extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,androidx.health.connect.client.records.ExerciseRoute> {
+  public final class ExerciseRouteRequestContract extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,androidx.health.connect.client.records.ExerciseRoute?> {
     ctor public ExerciseRouteRequestContract();
     method public android.content.Intent createIntent(android.content.Context context, String input);
     method public androidx.health.connect.client.records.ExerciseRoute? parseResult(int resultCode, android.content.Intent? intent);
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClientTest.kt
index 2c6a3bc..31c8df8 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClientTest.kt
@@ -208,7 +208,7 @@
         assertThat(fakeService.registeredCallbacks).hasSize(2)
         // Stub is not reused.
         assertThat(fakeService.registeredCallbacks[0]).isNotSameInstanceAs(
-            fakeService.registeredCallbacks[1]);
+            fakeService.registeredCallbacks[1])
     }
 
     @Test
diff --git a/kruth/kruth/api/current.txt b/kruth/kruth/api/current.txt
index c59b100..857e856 100644
--- a/kruth/kruth/api/current.txt
+++ b/kruth/kruth/api/current.txt
@@ -265,56 +265,56 @@
     method public void inOrder();
   }
 
-  public final class PrimitiveBooleanArraySubject extends androidx.kruth.Subject<boolean[]> {
+  public final class PrimitiveBooleanArraySubject extends androidx.kruth.Subject<boolean[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Boolean> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveByteArraySubject extends androidx.kruth.Subject<byte[]> {
+  public final class PrimitiveByteArraySubject extends androidx.kruth.Subject<byte[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Byte> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveCharArraySubject extends androidx.kruth.Subject<char[]> {
+  public final class PrimitiveCharArraySubject extends androidx.kruth.Subject<char[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Character> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveDoubleArraySubject extends androidx.kruth.Subject<double[]> {
+  public final class PrimitiveDoubleArraySubject extends androidx.kruth.Subject<double[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Double> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveFloatArraySubject extends androidx.kruth.Subject<float[]> {
+  public final class PrimitiveFloatArraySubject extends androidx.kruth.Subject<float[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Float> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveIntArraySubject extends androidx.kruth.Subject<int[]> {
+  public final class PrimitiveIntArraySubject extends androidx.kruth.Subject<int[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Integer> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveLongArraySubject extends androidx.kruth.Subject<long[]> {
+  public final class PrimitiveLongArraySubject extends androidx.kruth.Subject<long[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Long> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveShortArraySubject extends androidx.kruth.Subject<short[]> {
+  public final class PrimitiveShortArraySubject extends androidx.kruth.Subject<short[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Short> asList();
     method public void hasLength(int length);
     method public void isEmpty();
diff --git a/kruth/kruth/api/restricted_current.txt b/kruth/kruth/api/restricted_current.txt
index d387ad6..90ec259 100644
--- a/kruth/kruth/api/restricted_current.txt
+++ b/kruth/kruth/api/restricted_current.txt
@@ -265,56 +265,56 @@
     method public void inOrder();
   }
 
-  public final class PrimitiveBooleanArraySubject extends androidx.kruth.Subject<boolean[]> {
+  public final class PrimitiveBooleanArraySubject extends androidx.kruth.Subject<boolean[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Boolean> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveByteArraySubject extends androidx.kruth.Subject<byte[]> {
+  public final class PrimitiveByteArraySubject extends androidx.kruth.Subject<byte[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Byte> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveCharArraySubject extends androidx.kruth.Subject<char[]> {
+  public final class PrimitiveCharArraySubject extends androidx.kruth.Subject<char[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Character> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveDoubleArraySubject extends androidx.kruth.Subject<double[]> {
+  public final class PrimitiveDoubleArraySubject extends androidx.kruth.Subject<double[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Double> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveFloatArraySubject extends androidx.kruth.Subject<float[]> {
+  public final class PrimitiveFloatArraySubject extends androidx.kruth.Subject<float[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Float> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveIntArraySubject extends androidx.kruth.Subject<int[]> {
+  public final class PrimitiveIntArraySubject extends androidx.kruth.Subject<int[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Integer> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveLongArraySubject extends androidx.kruth.Subject<long[]> {
+  public final class PrimitiveLongArraySubject extends androidx.kruth.Subject<long[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Long> asList();
     method public void hasLength(int length);
     method public void isEmpty();
     method public void isNotEmpty();
   }
 
-  public final class PrimitiveShortArraySubject extends androidx.kruth.Subject<short[]> {
+  public final class PrimitiveShortArraySubject extends androidx.kruth.Subject<short[]?> {
     method public androidx.kruth.IterableSubject<java.lang.Short> asList();
     method public void hasLength(int length);
     method public void isEmpty();
diff --git a/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/ExpectTest.kt b/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/ExpectTest.kt
index 59e6cf8..4751c68 100644
--- a/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/ExpectTest.kt
+++ b/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/ExpectTest.kt
@@ -25,6 +25,7 @@
 import kotlin.test.assertFailsWith
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.rules.ExpectedException
@@ -153,11 +154,11 @@
     @Test
     fun bash() = runTest {
         val results = mutableListOf<Deferred<*>>()
-        repeat(1000) {
+        repeat(500) {
             results.add(async { expect.that(3).isEqualTo(4) })
         }
-        results.forEach { it.await() }
-        thrown.expectMessage("1000 expectations failed:")
+        results.awaitAll()
+        thrown.expectMessage("500 expectations failed:")
     }
 
     @Test
diff --git a/leanback/leanback-preference/api/1.2.0-beta01.txt b/leanback/leanback-preference/api/1.2.0-beta01.txt
index 55e5b84..6ebc263 100644
--- a/leanback/leanback-preference/api/1.2.0-beta01.txt
+++ b/leanback/leanback-preference/api/1.2.0-beta01.txt
@@ -26,7 +26,7 @@
     method @Deprecated public void onSaveInstanceState(android.os.Bundle!);
   }
 
-  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterMulti extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
+  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterMulti extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
     ctor @Deprecated public LeanbackListPreferenceDialogFragment.AdapterMulti(CharSequence![]!, CharSequence![]!, java.util.Set<java.lang.String!>!);
     method @Deprecated public int getItemCount();
     method @Deprecated public void onBindViewHolder(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!, int);
@@ -34,7 +34,7 @@
     method @Deprecated public void onItemClick(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!);
   }
 
-  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterSingle extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
+  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterSingle extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
     ctor @Deprecated public LeanbackListPreferenceDialogFragment.AdapterSingle(CharSequence![]!, CharSequence![]!, CharSequence!);
     method @Deprecated public int getItemCount();
     method @Deprecated public void onBindViewHolder(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!, int);
diff --git a/leanback/leanback-preference/api/current.txt b/leanback/leanback-preference/api/current.txt
index 55e5b84..6ebc263 100644
--- a/leanback/leanback-preference/api/current.txt
+++ b/leanback/leanback-preference/api/current.txt
@@ -26,7 +26,7 @@
     method @Deprecated public void onSaveInstanceState(android.os.Bundle!);
   }
 
-  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterMulti extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
+  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterMulti extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
     ctor @Deprecated public LeanbackListPreferenceDialogFragment.AdapterMulti(CharSequence![]!, CharSequence![]!, java.util.Set<java.lang.String!>!);
     method @Deprecated public int getItemCount();
     method @Deprecated public void onBindViewHolder(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!, int);
@@ -34,7 +34,7 @@
     method @Deprecated public void onItemClick(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!);
   }
 
-  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterSingle extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
+  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterSingle extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
     ctor @Deprecated public LeanbackListPreferenceDialogFragment.AdapterSingle(CharSequence![]!, CharSequence![]!, CharSequence!);
     method @Deprecated public int getItemCount();
     method @Deprecated public void onBindViewHolder(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!, int);
diff --git a/leanback/leanback-preference/api/restricted_1.2.0-beta01.txt b/leanback/leanback-preference/api/restricted_1.2.0-beta01.txt
index 16b6c76..56d892c 100644
--- a/leanback/leanback-preference/api/restricted_1.2.0-beta01.txt
+++ b/leanback/leanback-preference/api/restricted_1.2.0-beta01.txt
@@ -27,7 +27,7 @@
     method @Deprecated public void onSaveInstanceState(android.os.Bundle!);
   }
 
-  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterMulti extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
+  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterMulti extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
     ctor @Deprecated public LeanbackListPreferenceDialogFragment.AdapterMulti(CharSequence![]!, CharSequence![]!, java.util.Set<java.lang.String!>!);
     method @Deprecated public int getItemCount();
     method @Deprecated public void onBindViewHolder(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!, int);
@@ -35,7 +35,7 @@
     method @Deprecated public void onItemClick(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!);
   }
 
-  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterSingle extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
+  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterSingle extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
     ctor @Deprecated public LeanbackListPreferenceDialogFragment.AdapterSingle(CharSequence![]!, CharSequence![]!, CharSequence!);
     method @Deprecated public int getItemCount();
     method @Deprecated public void onBindViewHolder(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!, int);
diff --git a/leanback/leanback-preference/api/restricted_current.txt b/leanback/leanback-preference/api/restricted_current.txt
index 16b6c76..56d892c 100644
--- a/leanback/leanback-preference/api/restricted_current.txt
+++ b/leanback/leanback-preference/api/restricted_current.txt
@@ -27,7 +27,7 @@
     method @Deprecated public void onSaveInstanceState(android.os.Bundle!);
   }
 
-  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterMulti extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
+  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterMulti extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
     ctor @Deprecated public LeanbackListPreferenceDialogFragment.AdapterMulti(CharSequence![]!, CharSequence![]!, java.util.Set<java.lang.String!>!);
     method @Deprecated public int getItemCount();
     method @Deprecated public void onBindViewHolder(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!, int);
@@ -35,7 +35,7 @@
     method @Deprecated public void onItemClick(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!);
   }
 
-  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterSingle extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
+  @Deprecated public class LeanbackListPreferenceDialogFragment.AdapterSingle extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!> implements androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder.OnItemClickListener {
     ctor @Deprecated public LeanbackListPreferenceDialogFragment.AdapterSingle(CharSequence![]!, CharSequence![]!, CharSequence!);
     method @Deprecated public int getItemCount();
     method @Deprecated public void onBindViewHolder(androidx.leanback.preference.LeanbackListPreferenceDialogFragment.ViewHolder!, int);
diff --git a/leanback/leanback/api/1.2.0-beta01.txt b/leanback/leanback/api/1.2.0-beta01.txt
index e783a1d..e46ff1c 100644
--- a/leanback/leanback/api/1.2.0-beta01.txt
+++ b/leanback/leanback/api/1.2.0-beta01.txt
@@ -149,7 +149,7 @@
     method @Deprecated public void showTitleView(boolean);
   }
 
-  @Deprecated public static class BrowseFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseFragment.FragmentFactory<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class BrowseFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseFragment.FragmentFactory<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public BrowseFragment.ListRowFragmentFactory();
     method @Deprecated public androidx.leanback.app.RowsFragment! createFragment(Object!);
   }
@@ -249,7 +249,7 @@
     method public void showTitleView(boolean);
   }
 
-  public static class BrowseSupportFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseSupportFragment.FragmentFactory<androidx.leanback.app.RowsSupportFragment> {
+  public static class BrowseSupportFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseSupportFragment.FragmentFactory<androidx.leanback.app.RowsSupportFragment!> {
     ctor public BrowseSupportFragment.ListRowFragmentFactory();
     method public androidx.leanback.app.RowsSupportFragment! createFragment(Object!);
   }
@@ -822,11 +822,11 @@
     method @Deprecated public void setSelectedPosition(int, boolean, androidx.leanback.widget.Presenter.ViewHolderTask!);
   }
 
-  @Deprecated public static class RowsFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentAdapter<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class RowsFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentAdapter<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public RowsFragment.MainFragmentAdapter(androidx.leanback.app.RowsFragment!);
   }
 
-  @Deprecated public static class RowsFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class RowsFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public RowsFragment.MainFragmentRowsAdapter(androidx.leanback.app.RowsFragment!);
   }
 
@@ -861,11 +861,11 @@
     method public void setSelectedPosition(int, boolean, androidx.leanback.widget.Presenter.ViewHolderTask!);
   }
 
-  public static class RowsSupportFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentAdapter<androidx.leanback.app.RowsSupportFragment> {
+  public static class RowsSupportFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentAdapter<androidx.leanback.app.RowsSupportFragment!> {
     ctor public RowsSupportFragment.MainFragmentAdapter(androidx.leanback.app.RowsSupportFragment!);
   }
 
-  public static class RowsSupportFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsSupportFragment> {
+  public static class RowsSupportFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsSupportFragment!> {
     ctor public RowsSupportFragment.MainFragmentRowsAdapter(androidx.leanback.app.RowsSupportFragment!);
   }
 
@@ -1137,7 +1137,7 @@
     method public boolean setDataSource(android.net.Uri!);
   }
 
-  public class PlaybackBannerControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T> {
+  public class PlaybackBannerControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T!> {
     ctor public PlaybackBannerControlGlue(android.content.Context, int[], int[], T!);
     ctor public PlaybackBannerControlGlue(android.content.Context, int[], T!);
     method public int[] getFastForwardSpeeds();
@@ -1321,7 +1321,7 @@
     method public void onVideoSizeChanged(int, int);
   }
 
-  public class PlaybackTransportControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T> {
+  public class PlaybackTransportControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T!> {
     ctor public PlaybackTransportControlGlue(android.content.Context!, T!);
     method public final androidx.leanback.widget.PlaybackSeekDataProvider! getSeekProvider();
     method public final boolean isSeekEnabled();
@@ -1840,7 +1840,7 @@
     field public static final int NO_CHECK_SET = 0; // 0x0
   }
 
-  public static class GuidedAction.Builder extends androidx.leanback.widget.GuidedAction.BuilderBase<androidx.leanback.widget.GuidedAction.Builder> {
+  public static class GuidedAction.Builder extends androidx.leanback.widget.GuidedAction.BuilderBase<androidx.leanback.widget.GuidedAction.Builder!> {
     ctor @Deprecated public GuidedAction.Builder();
     ctor public GuidedAction.Builder(android.content.Context?);
     method public androidx.leanback.widget.GuidedAction build();
@@ -1899,7 +1899,7 @@
     method public void onAutofill(android.view.View!);
   }
 
-  public class GuidedActionDiffCallback extends androidx.leanback.widget.DiffCallback<androidx.leanback.widget.GuidedAction> {
+  public class GuidedActionDiffCallback extends androidx.leanback.widget.DiffCallback<androidx.leanback.widget.GuidedAction!> {
     ctor public GuidedActionDiffCallback();
     method public boolean areContentsTheSame(androidx.leanback.widget.GuidedAction, androidx.leanback.widget.GuidedAction);
     method public boolean areItemsTheSame(androidx.leanback.widget.GuidedAction, androidx.leanback.widget.GuidedAction);
@@ -1993,12 +1993,12 @@
     method public void setDate(long);
   }
 
-  public static final class GuidedDatePickerAction.Builder extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase<androidx.leanback.widget.GuidedDatePickerAction.Builder> {
+  public static final class GuidedDatePickerAction.Builder extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase<androidx.leanback.widget.GuidedDatePickerAction.Builder!> {
     ctor public GuidedDatePickerAction.Builder(android.content.Context);
     method public androidx.leanback.widget.GuidedDatePickerAction build();
   }
 
-  public abstract static class GuidedDatePickerAction.BuilderBase<B extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase> extends androidx.leanback.widget.GuidedAction.BuilderBase<B> {
+  public abstract static class GuidedDatePickerAction.BuilderBase<B extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase> extends androidx.leanback.widget.GuidedAction.BuilderBase<B!> {
     ctor public GuidedDatePickerAction.BuilderBase(android.content.Context);
     method protected final void applyDatePickerValues(androidx.leanback.widget.GuidedDatePickerAction);
     method public B! date(long);
@@ -2258,10 +2258,10 @@
     method public void onActionClicked(androidx.leanback.widget.Action);
   }
 
-  public interface OnItemViewClickedListener extends androidx.leanback.widget.BaseOnItemViewClickedListener<androidx.leanback.widget.Row> {
+  public interface OnItemViewClickedListener extends androidx.leanback.widget.BaseOnItemViewClickedListener<androidx.leanback.widget.Row!> {
   }
 
-  public interface OnItemViewSelectedListener extends androidx.leanback.widget.BaseOnItemViewSelectedListener<androidx.leanback.widget.Row> {
+  public interface OnItemViewSelectedListener extends androidx.leanback.widget.BaseOnItemViewSelectedListener<androidx.leanback.widget.Row!> {
   }
 
   public class PageRow extends androidx.leanback.widget.Row {
@@ -2282,7 +2282,7 @@
     method @CallSuper public void updateValues();
   }
 
-  public static class Parallax.FloatProperty extends android.util.Property<androidx.leanback.widget.Parallax,java.lang.Float> {
+  public static class Parallax.FloatProperty extends android.util.Property<androidx.leanback.widget.Parallax!,java.lang.Float!> {
     ctor public Parallax.FloatProperty(String!, int);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! at(float, float);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! atAbsolute(float);
@@ -2298,7 +2298,7 @@
     field public static final float UNKNOWN_BEFORE = -3.4028235E38f;
   }
 
-  public static class Parallax.IntProperty extends android.util.Property<androidx.leanback.widget.Parallax,java.lang.Integer> {
+  public static class Parallax.IntProperty extends android.util.Property<androidx.leanback.widget.Parallax!,java.lang.Integer!> {
     ctor public Parallax.IntProperty(String!, int);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! at(int, float);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! atAbsolute(int);
@@ -2613,7 +2613,7 @@
     method public void unselect();
   }
 
-  public class RecyclerViewParallax extends androidx.leanback.widget.Parallax<androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty> {
+  public class RecyclerViewParallax extends androidx.leanback.widget.Parallax<androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty!> {
     ctor public RecyclerViewParallax();
     method public androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty! createProperty(String!, int);
     method public float getMaxValue();
diff --git a/leanback/leanback/api/current.txt b/leanback/leanback/api/current.txt
index e783a1d..e46ff1c 100644
--- a/leanback/leanback/api/current.txt
+++ b/leanback/leanback/api/current.txt
@@ -149,7 +149,7 @@
     method @Deprecated public void showTitleView(boolean);
   }
 
-  @Deprecated public static class BrowseFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseFragment.FragmentFactory<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class BrowseFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseFragment.FragmentFactory<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public BrowseFragment.ListRowFragmentFactory();
     method @Deprecated public androidx.leanback.app.RowsFragment! createFragment(Object!);
   }
@@ -249,7 +249,7 @@
     method public void showTitleView(boolean);
   }
 
-  public static class BrowseSupportFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseSupportFragment.FragmentFactory<androidx.leanback.app.RowsSupportFragment> {
+  public static class BrowseSupportFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseSupportFragment.FragmentFactory<androidx.leanback.app.RowsSupportFragment!> {
     ctor public BrowseSupportFragment.ListRowFragmentFactory();
     method public androidx.leanback.app.RowsSupportFragment! createFragment(Object!);
   }
@@ -822,11 +822,11 @@
     method @Deprecated public void setSelectedPosition(int, boolean, androidx.leanback.widget.Presenter.ViewHolderTask!);
   }
 
-  @Deprecated public static class RowsFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentAdapter<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class RowsFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentAdapter<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public RowsFragment.MainFragmentAdapter(androidx.leanback.app.RowsFragment!);
   }
 
-  @Deprecated public static class RowsFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class RowsFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public RowsFragment.MainFragmentRowsAdapter(androidx.leanback.app.RowsFragment!);
   }
 
@@ -861,11 +861,11 @@
     method public void setSelectedPosition(int, boolean, androidx.leanback.widget.Presenter.ViewHolderTask!);
   }
 
-  public static class RowsSupportFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentAdapter<androidx.leanback.app.RowsSupportFragment> {
+  public static class RowsSupportFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentAdapter<androidx.leanback.app.RowsSupportFragment!> {
     ctor public RowsSupportFragment.MainFragmentAdapter(androidx.leanback.app.RowsSupportFragment!);
   }
 
-  public static class RowsSupportFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsSupportFragment> {
+  public static class RowsSupportFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsSupportFragment!> {
     ctor public RowsSupportFragment.MainFragmentRowsAdapter(androidx.leanback.app.RowsSupportFragment!);
   }
 
@@ -1137,7 +1137,7 @@
     method public boolean setDataSource(android.net.Uri!);
   }
 
-  public class PlaybackBannerControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T> {
+  public class PlaybackBannerControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T!> {
     ctor public PlaybackBannerControlGlue(android.content.Context, int[], int[], T!);
     ctor public PlaybackBannerControlGlue(android.content.Context, int[], T!);
     method public int[] getFastForwardSpeeds();
@@ -1321,7 +1321,7 @@
     method public void onVideoSizeChanged(int, int);
   }
 
-  public class PlaybackTransportControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T> {
+  public class PlaybackTransportControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T!> {
     ctor public PlaybackTransportControlGlue(android.content.Context!, T!);
     method public final androidx.leanback.widget.PlaybackSeekDataProvider! getSeekProvider();
     method public final boolean isSeekEnabled();
@@ -1840,7 +1840,7 @@
     field public static final int NO_CHECK_SET = 0; // 0x0
   }
 
-  public static class GuidedAction.Builder extends androidx.leanback.widget.GuidedAction.BuilderBase<androidx.leanback.widget.GuidedAction.Builder> {
+  public static class GuidedAction.Builder extends androidx.leanback.widget.GuidedAction.BuilderBase<androidx.leanback.widget.GuidedAction.Builder!> {
     ctor @Deprecated public GuidedAction.Builder();
     ctor public GuidedAction.Builder(android.content.Context?);
     method public androidx.leanback.widget.GuidedAction build();
@@ -1899,7 +1899,7 @@
     method public void onAutofill(android.view.View!);
   }
 
-  public class GuidedActionDiffCallback extends androidx.leanback.widget.DiffCallback<androidx.leanback.widget.GuidedAction> {
+  public class GuidedActionDiffCallback extends androidx.leanback.widget.DiffCallback<androidx.leanback.widget.GuidedAction!> {
     ctor public GuidedActionDiffCallback();
     method public boolean areContentsTheSame(androidx.leanback.widget.GuidedAction, androidx.leanback.widget.GuidedAction);
     method public boolean areItemsTheSame(androidx.leanback.widget.GuidedAction, androidx.leanback.widget.GuidedAction);
@@ -1993,12 +1993,12 @@
     method public void setDate(long);
   }
 
-  public static final class GuidedDatePickerAction.Builder extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase<androidx.leanback.widget.GuidedDatePickerAction.Builder> {
+  public static final class GuidedDatePickerAction.Builder extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase<androidx.leanback.widget.GuidedDatePickerAction.Builder!> {
     ctor public GuidedDatePickerAction.Builder(android.content.Context);
     method public androidx.leanback.widget.GuidedDatePickerAction build();
   }
 
-  public abstract static class GuidedDatePickerAction.BuilderBase<B extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase> extends androidx.leanback.widget.GuidedAction.BuilderBase<B> {
+  public abstract static class GuidedDatePickerAction.BuilderBase<B extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase> extends androidx.leanback.widget.GuidedAction.BuilderBase<B!> {
     ctor public GuidedDatePickerAction.BuilderBase(android.content.Context);
     method protected final void applyDatePickerValues(androidx.leanback.widget.GuidedDatePickerAction);
     method public B! date(long);
@@ -2258,10 +2258,10 @@
     method public void onActionClicked(androidx.leanback.widget.Action);
   }
 
-  public interface OnItemViewClickedListener extends androidx.leanback.widget.BaseOnItemViewClickedListener<androidx.leanback.widget.Row> {
+  public interface OnItemViewClickedListener extends androidx.leanback.widget.BaseOnItemViewClickedListener<androidx.leanback.widget.Row!> {
   }
 
-  public interface OnItemViewSelectedListener extends androidx.leanback.widget.BaseOnItemViewSelectedListener<androidx.leanback.widget.Row> {
+  public interface OnItemViewSelectedListener extends androidx.leanback.widget.BaseOnItemViewSelectedListener<androidx.leanback.widget.Row!> {
   }
 
   public class PageRow extends androidx.leanback.widget.Row {
@@ -2282,7 +2282,7 @@
     method @CallSuper public void updateValues();
   }
 
-  public static class Parallax.FloatProperty extends android.util.Property<androidx.leanback.widget.Parallax,java.lang.Float> {
+  public static class Parallax.FloatProperty extends android.util.Property<androidx.leanback.widget.Parallax!,java.lang.Float!> {
     ctor public Parallax.FloatProperty(String!, int);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! at(float, float);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! atAbsolute(float);
@@ -2298,7 +2298,7 @@
     field public static final float UNKNOWN_BEFORE = -3.4028235E38f;
   }
 
-  public static class Parallax.IntProperty extends android.util.Property<androidx.leanback.widget.Parallax,java.lang.Integer> {
+  public static class Parallax.IntProperty extends android.util.Property<androidx.leanback.widget.Parallax!,java.lang.Integer!> {
     ctor public Parallax.IntProperty(String!, int);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! at(int, float);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! atAbsolute(int);
@@ -2613,7 +2613,7 @@
     method public void unselect();
   }
 
-  public class RecyclerViewParallax extends androidx.leanback.widget.Parallax<androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty> {
+  public class RecyclerViewParallax extends androidx.leanback.widget.Parallax<androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty!> {
     ctor public RecyclerViewParallax();
     method public androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty! createProperty(String!, int);
     method public float getMaxValue();
diff --git a/leanback/leanback/api/restricted_1.2.0-beta01.txt b/leanback/leanback/api/restricted_1.2.0-beta01.txt
index 8bdf344..daf1928 100644
--- a/leanback/leanback/api/restricted_1.2.0-beta01.txt
+++ b/leanback/leanback/api/restricted_1.2.0-beta01.txt
@@ -171,7 +171,7 @@
     method @Deprecated public void showTitleView(boolean);
   }
 
-  @Deprecated public static class BrowseFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseFragment.FragmentFactory<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class BrowseFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseFragment.FragmentFactory<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public BrowseFragment.ListRowFragmentFactory();
     method @Deprecated public androidx.leanback.app.RowsFragment! createFragment(Object!);
   }
@@ -271,7 +271,7 @@
     method public void showTitleView(boolean);
   }
 
-  public static class BrowseSupportFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseSupportFragment.FragmentFactory<androidx.leanback.app.RowsSupportFragment> {
+  public static class BrowseSupportFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseSupportFragment.FragmentFactory<androidx.leanback.app.RowsSupportFragment!> {
     ctor public BrowseSupportFragment.ListRowFragmentFactory();
     method public androidx.leanback.app.RowsSupportFragment! createFragment(Object!);
   }
@@ -863,11 +863,11 @@
     method @Deprecated public void setSelectedPosition(int, boolean, androidx.leanback.widget.Presenter.ViewHolderTask!);
   }
 
-  @Deprecated public static class RowsFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentAdapter<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class RowsFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentAdapter<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public RowsFragment.MainFragmentAdapter(androidx.leanback.app.RowsFragment!);
   }
 
-  @Deprecated public static class RowsFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class RowsFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public RowsFragment.MainFragmentRowsAdapter(androidx.leanback.app.RowsFragment!);
   }
 
@@ -902,11 +902,11 @@
     method public void setSelectedPosition(int, boolean, androidx.leanback.widget.Presenter.ViewHolderTask!);
   }
 
-  public static class RowsSupportFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentAdapter<androidx.leanback.app.RowsSupportFragment> {
+  public static class RowsSupportFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentAdapter<androidx.leanback.app.RowsSupportFragment!> {
     ctor public RowsSupportFragment.MainFragmentAdapter(androidx.leanback.app.RowsSupportFragment!);
   }
 
-  public static class RowsSupportFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsSupportFragment> {
+  public static class RowsSupportFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsSupportFragment!> {
     ctor public RowsSupportFragment.MainFragmentRowsAdapter(androidx.leanback.app.RowsSupportFragment!);
   }
 
@@ -1211,7 +1211,7 @@
     field @Deprecated protected final androidx.leanback.widget.PlaybackControlsRow.ThumbsUpAction! mThumbsUpAction;
   }
 
-  public class PlaybackBannerControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T> {
+  public class PlaybackBannerControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T!> {
     ctor public PlaybackBannerControlGlue(android.content.Context, int[], int[], T!);
     ctor public PlaybackBannerControlGlue(android.content.Context, int[], T!);
     method public int[] getFastForwardSpeeds();
@@ -1398,7 +1398,7 @@
     method public void onVideoSizeChanged(int, int);
   }
 
-  public class PlaybackTransportControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T> {
+  public class PlaybackTransportControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T!> {
     ctor public PlaybackTransportControlGlue(android.content.Context!, T!);
     method public final androidx.leanback.widget.PlaybackSeekDataProvider! getSeekProvider();
     method public final boolean isSeekEnabled();
@@ -2002,7 +2002,7 @@
     field public static final int NO_CHECK_SET = 0; // 0x0
   }
 
-  public static class GuidedAction.Builder extends androidx.leanback.widget.GuidedAction.BuilderBase<androidx.leanback.widget.GuidedAction.Builder> {
+  public static class GuidedAction.Builder extends androidx.leanback.widget.GuidedAction.BuilderBase<androidx.leanback.widget.GuidedAction.Builder!> {
     ctor @Deprecated public GuidedAction.Builder();
     ctor public GuidedAction.Builder(android.content.Context?);
     method public androidx.leanback.widget.GuidedAction build();
@@ -2106,7 +2106,7 @@
     method public void onAutofill(android.view.View!);
   }
 
-  public class GuidedActionDiffCallback extends androidx.leanback.widget.DiffCallback<androidx.leanback.widget.GuidedAction> {
+  public class GuidedActionDiffCallback extends androidx.leanback.widget.DiffCallback<androidx.leanback.widget.GuidedAction!> {
     ctor public GuidedActionDiffCallback();
     method public boolean areContentsTheSame(androidx.leanback.widget.GuidedAction, androidx.leanback.widget.GuidedAction);
     method public boolean areItemsTheSame(androidx.leanback.widget.GuidedAction, androidx.leanback.widget.GuidedAction);
@@ -2201,12 +2201,12 @@
     method public void setDate(long);
   }
 
-  public static final class GuidedDatePickerAction.Builder extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase<androidx.leanback.widget.GuidedDatePickerAction.Builder> {
+  public static final class GuidedDatePickerAction.Builder extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase<androidx.leanback.widget.GuidedDatePickerAction.Builder!> {
     ctor public GuidedDatePickerAction.Builder(android.content.Context);
     method public androidx.leanback.widget.GuidedDatePickerAction build();
   }
 
-  public abstract static class GuidedDatePickerAction.BuilderBase<B extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase> extends androidx.leanback.widget.GuidedAction.BuilderBase<B> {
+  public abstract static class GuidedDatePickerAction.BuilderBase<B extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase> extends androidx.leanback.widget.GuidedAction.BuilderBase<B!> {
     ctor public GuidedDatePickerAction.BuilderBase(android.content.Context);
     method protected final void applyDatePickerValues(androidx.leanback.widget.GuidedDatePickerAction);
     method public B! date(long);
@@ -2484,10 +2484,10 @@
     method public void onActionClicked(androidx.leanback.widget.Action);
   }
 
-  public interface OnItemViewClickedListener extends androidx.leanback.widget.BaseOnItemViewClickedListener<androidx.leanback.widget.Row> {
+  public interface OnItemViewClickedListener extends androidx.leanback.widget.BaseOnItemViewClickedListener<androidx.leanback.widget.Row!> {
   }
 
-  public interface OnItemViewSelectedListener extends androidx.leanback.widget.BaseOnItemViewSelectedListener<androidx.leanback.widget.Row> {
+  public interface OnItemViewSelectedListener extends androidx.leanback.widget.BaseOnItemViewSelectedListener<androidx.leanback.widget.Row!> {
   }
 
   public class PageRow extends androidx.leanback.widget.Row {
@@ -2530,7 +2530,7 @@
     method @CallSuper public void updateValues();
   }
 
-  public static class Parallax.FloatProperty extends android.util.Property<androidx.leanback.widget.Parallax,java.lang.Float> {
+  public static class Parallax.FloatProperty extends android.util.Property<androidx.leanback.widget.Parallax!,java.lang.Float!> {
     ctor public Parallax.FloatProperty(String!, int);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! at(float, float);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! atAbsolute(float);
@@ -2546,7 +2546,7 @@
     field public static final float UNKNOWN_BEFORE = -3.4028235E38f;
   }
 
-  public static class Parallax.IntProperty extends android.util.Property<androidx.leanback.widget.Parallax,java.lang.Integer> {
+  public static class Parallax.IntProperty extends android.util.Property<androidx.leanback.widget.Parallax!,java.lang.Integer!> {
     ctor public Parallax.IntProperty(String!, int);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! at(int, float);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! atAbsolute(int);
@@ -2870,7 +2870,7 @@
     method public void unselect();
   }
 
-  public class RecyclerViewParallax extends androidx.leanback.widget.Parallax<androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty> {
+  public class RecyclerViewParallax extends androidx.leanback.widget.Parallax<androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty!> {
     ctor public RecyclerViewParallax();
     method public androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty! createProperty(String!, int);
     method public float getMaxValue();
diff --git a/leanback/leanback/api/restricted_current.txt b/leanback/leanback/api/restricted_current.txt
index 8bdf344..daf1928 100644
--- a/leanback/leanback/api/restricted_current.txt
+++ b/leanback/leanback/api/restricted_current.txt
@@ -171,7 +171,7 @@
     method @Deprecated public void showTitleView(boolean);
   }
 
-  @Deprecated public static class BrowseFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseFragment.FragmentFactory<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class BrowseFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseFragment.FragmentFactory<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public BrowseFragment.ListRowFragmentFactory();
     method @Deprecated public androidx.leanback.app.RowsFragment! createFragment(Object!);
   }
@@ -271,7 +271,7 @@
     method public void showTitleView(boolean);
   }
 
-  public static class BrowseSupportFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseSupportFragment.FragmentFactory<androidx.leanback.app.RowsSupportFragment> {
+  public static class BrowseSupportFragment.ListRowFragmentFactory extends androidx.leanback.app.BrowseSupportFragment.FragmentFactory<androidx.leanback.app.RowsSupportFragment!> {
     ctor public BrowseSupportFragment.ListRowFragmentFactory();
     method public androidx.leanback.app.RowsSupportFragment! createFragment(Object!);
   }
@@ -863,11 +863,11 @@
     method @Deprecated public void setSelectedPosition(int, boolean, androidx.leanback.widget.Presenter.ViewHolderTask!);
   }
 
-  @Deprecated public static class RowsFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentAdapter<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class RowsFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentAdapter<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public RowsFragment.MainFragmentAdapter(androidx.leanback.app.RowsFragment!);
   }
 
-  @Deprecated public static class RowsFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsFragment> {
+  @Deprecated public static class RowsFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsFragment!> {
     ctor @Deprecated public RowsFragment.MainFragmentRowsAdapter(androidx.leanback.app.RowsFragment!);
   }
 
@@ -902,11 +902,11 @@
     method public void setSelectedPosition(int, boolean, androidx.leanback.widget.Presenter.ViewHolderTask!);
   }
 
-  public static class RowsSupportFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentAdapter<androidx.leanback.app.RowsSupportFragment> {
+  public static class RowsSupportFragment.MainFragmentAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentAdapter<androidx.leanback.app.RowsSupportFragment!> {
     ctor public RowsSupportFragment.MainFragmentAdapter(androidx.leanback.app.RowsSupportFragment!);
   }
 
-  public static class RowsSupportFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsSupportFragment> {
+  public static class RowsSupportFragment.MainFragmentRowsAdapter extends androidx.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter<androidx.leanback.app.RowsSupportFragment!> {
     ctor public RowsSupportFragment.MainFragmentRowsAdapter(androidx.leanback.app.RowsSupportFragment!);
   }
 
@@ -1211,7 +1211,7 @@
     field @Deprecated protected final androidx.leanback.widget.PlaybackControlsRow.ThumbsUpAction! mThumbsUpAction;
   }
 
-  public class PlaybackBannerControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T> {
+  public class PlaybackBannerControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T!> {
     ctor public PlaybackBannerControlGlue(android.content.Context, int[], int[], T!);
     ctor public PlaybackBannerControlGlue(android.content.Context, int[], T!);
     method public int[] getFastForwardSpeeds();
@@ -1398,7 +1398,7 @@
     method public void onVideoSizeChanged(int, int);
   }
 
-  public class PlaybackTransportControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T> {
+  public class PlaybackTransportControlGlue<T extends androidx.leanback.media.PlayerAdapter> extends androidx.leanback.media.PlaybackBaseControlGlue<T!> {
     ctor public PlaybackTransportControlGlue(android.content.Context!, T!);
     method public final androidx.leanback.widget.PlaybackSeekDataProvider! getSeekProvider();
     method public final boolean isSeekEnabled();
@@ -2002,7 +2002,7 @@
     field public static final int NO_CHECK_SET = 0; // 0x0
   }
 
-  public static class GuidedAction.Builder extends androidx.leanback.widget.GuidedAction.BuilderBase<androidx.leanback.widget.GuidedAction.Builder> {
+  public static class GuidedAction.Builder extends androidx.leanback.widget.GuidedAction.BuilderBase<androidx.leanback.widget.GuidedAction.Builder!> {
     ctor @Deprecated public GuidedAction.Builder();
     ctor public GuidedAction.Builder(android.content.Context?);
     method public androidx.leanback.widget.GuidedAction build();
@@ -2106,7 +2106,7 @@
     method public void onAutofill(android.view.View!);
   }
 
-  public class GuidedActionDiffCallback extends androidx.leanback.widget.DiffCallback<androidx.leanback.widget.GuidedAction> {
+  public class GuidedActionDiffCallback extends androidx.leanback.widget.DiffCallback<androidx.leanback.widget.GuidedAction!> {
     ctor public GuidedActionDiffCallback();
     method public boolean areContentsTheSame(androidx.leanback.widget.GuidedAction, androidx.leanback.widget.GuidedAction);
     method public boolean areItemsTheSame(androidx.leanback.widget.GuidedAction, androidx.leanback.widget.GuidedAction);
@@ -2201,12 +2201,12 @@
     method public void setDate(long);
   }
 
-  public static final class GuidedDatePickerAction.Builder extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase<androidx.leanback.widget.GuidedDatePickerAction.Builder> {
+  public static final class GuidedDatePickerAction.Builder extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase<androidx.leanback.widget.GuidedDatePickerAction.Builder!> {
     ctor public GuidedDatePickerAction.Builder(android.content.Context);
     method public androidx.leanback.widget.GuidedDatePickerAction build();
   }
 
-  public abstract static class GuidedDatePickerAction.BuilderBase<B extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase> extends androidx.leanback.widget.GuidedAction.BuilderBase<B> {
+  public abstract static class GuidedDatePickerAction.BuilderBase<B extends androidx.leanback.widget.GuidedDatePickerAction.BuilderBase> extends androidx.leanback.widget.GuidedAction.BuilderBase<B!> {
     ctor public GuidedDatePickerAction.BuilderBase(android.content.Context);
     method protected final void applyDatePickerValues(androidx.leanback.widget.GuidedDatePickerAction);
     method public B! date(long);
@@ -2484,10 +2484,10 @@
     method public void onActionClicked(androidx.leanback.widget.Action);
   }
 
-  public interface OnItemViewClickedListener extends androidx.leanback.widget.BaseOnItemViewClickedListener<androidx.leanback.widget.Row> {
+  public interface OnItemViewClickedListener extends androidx.leanback.widget.BaseOnItemViewClickedListener<androidx.leanback.widget.Row!> {
   }
 
-  public interface OnItemViewSelectedListener extends androidx.leanback.widget.BaseOnItemViewSelectedListener<androidx.leanback.widget.Row> {
+  public interface OnItemViewSelectedListener extends androidx.leanback.widget.BaseOnItemViewSelectedListener<androidx.leanback.widget.Row!> {
   }
 
   public class PageRow extends androidx.leanback.widget.Row {
@@ -2530,7 +2530,7 @@
     method @CallSuper public void updateValues();
   }
 
-  public static class Parallax.FloatProperty extends android.util.Property<androidx.leanback.widget.Parallax,java.lang.Float> {
+  public static class Parallax.FloatProperty extends android.util.Property<androidx.leanback.widget.Parallax!,java.lang.Float!> {
     ctor public Parallax.FloatProperty(String!, int);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! at(float, float);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! atAbsolute(float);
@@ -2546,7 +2546,7 @@
     field public static final float UNKNOWN_BEFORE = -3.4028235E38f;
   }
 
-  public static class Parallax.IntProperty extends android.util.Property<androidx.leanback.widget.Parallax,java.lang.Integer> {
+  public static class Parallax.IntProperty extends android.util.Property<androidx.leanback.widget.Parallax!,java.lang.Integer!> {
     ctor public Parallax.IntProperty(String!, int);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! at(int, float);
     method public final androidx.leanback.widget.Parallax.PropertyMarkerValue! atAbsolute(int);
@@ -2870,7 +2870,7 @@
     method public void unselect();
   }
 
-  public class RecyclerViewParallax extends androidx.leanback.widget.Parallax<androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty> {
+  public class RecyclerViewParallax extends androidx.leanback.widget.Parallax<androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty!> {
     ctor public RecyclerViewParallax();
     method public androidx.leanback.widget.RecyclerViewParallax.ChildPositionProperty! createProperty(String!, int);
     method public float getMaxValue();
diff --git a/libraryversions.toml b/libraryversions.toml
index ba5de7f..1bbed7a 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -23,7 +23,7 @@
 CAR_APP = "1.7.0-alpha01"
 COLLECTION = "1.5.0-alpha01"
 COMPOSE = "1.7.0-alpha05"
-COMPOSE_COMPILER = "1.5.10"  # Update when preparing for a release
+COMPOSE_COMPILER = "1.5.11"  # Update when preparing for a release
 COMPOSE_MATERIAL3 = "1.3.0-alpha03"
 COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha09"
 COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha05"
@@ -75,7 +75,7 @@
 GRAPHICS_SHAPES = "1.0.0-beta01"
 GRIDLAYOUT = "1.1.0-beta02"
 HEALTH_CONNECT = "1.1.0-alpha08"
-HEALTH_SERVICES_CLIENT = "1.1.0-alpha02"
+HEALTH_SERVICES_CLIENT = "1.1.0-alpha03"
 HEIFWRITER = "1.1.0-alpha03"
 HILT = "1.2.0-rc01"
 HILT_NAVIGATION = "1.2.0-rc01"
diff --git a/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt b/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
index ad4d5df..cf14cdc 100644
--- a/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
+++ b/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
@@ -29,10 +29,10 @@
 import com.android.tools.lint.detector.api.UastLintUtils
 import com.android.tools.lint.detector.api.isKotlin
 import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiTypeParameter
 import com.intellij.psi.PsiVariable
 import com.intellij.psi.PsiWhiteSpace
 import com.intellij.psi.impl.source.PsiImmediateClassType
-import org.jetbrains.kotlin.asJava.elements.KtLightTypeParameter
 import org.jetbrains.kotlin.psi.KtCallExpression
 import org.jetbrains.kotlin.psi.KtCallableDeclaration
 import org.jetbrains.kotlin.psi.KtNullableType
@@ -102,7 +102,7 @@
                 // argument: `Boolean`
                 val typeReference = element.sourcePsi
                     ?.children
-                    ?.firstOrNull { it is KtTypeReference } as? KtTypeReference
+                    ?.firstNotNullOfOrNull { it as? KtTypeReference }
                 val typeArgument = typeReference?.typeElement?.typeArgumentsAsTypes?.singleOrNull()
                 if (typeArgument != null) {
                     return typeArgument
@@ -114,12 +114,12 @@
                 // argument: `Boolean`
                 val expression = element.sourcePsi
                     ?.children
-                    ?.firstOrNull { it is KtCallExpression } as? KtCallExpression
+                    ?.firstNotNullOfOrNull { it as? KtCallExpression }
                 return expression?.typeArguments?.singleOrNull()?.typeReference
             }
 
             override fun visitCallExpression(node: UCallExpression) {
-                if (!isKotlin(node.sourcePsi) || !methods.contains(node.methodName) ||
+                if (!isKotlin(node.lang) || !methods.contains(node.methodName) ||
                     !context.evaluator.isMemberInSubClassOf(
                             node.resolve()!!, "androidx.lifecycle.LiveData", false
                         )
@@ -175,6 +175,11 @@
             return null
         }
         val cls = classType.resolve().getUastParentOfType<UClass>()
+        if (cls != null && !isKotlin(cls.lang)) {
+            // If the type argument refers to a Java type,
+            // we won't get KtTypeReference anyway, so bail out early.
+            return null
+        }
         val parentPsiType = cls?.superClassType as PsiClassType
         if (parentPsiType.hasParameters()) {
             val parentTypeReference = cls.uastSuperTypes[0]
@@ -223,7 +228,7 @@
     private fun UCallExpression.isGenericTypeDefinition(): Boolean {
         val classType = typeArguments.singleOrNull() as? PsiImmediateClassType
         val resolveGenerics = classType?.resolveGenerics()
-        return resolveGenerics?.element is KtLightTypeParameter
+        return resolveGenerics?.element is PsiTypeParameter
     }
 
     /**
diff --git a/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt b/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
index 97e2074..03173d3 100644
--- a/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
+++ b/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
@@ -852,4 +852,166 @@
 1 errors, 0 warnings
         """)
     }
+
+    @Test
+    fun dataClassFromBinary_nonNull() {
+        check(
+            kotlin(
+                """
+                package com.example
+
+                import androidx.lifecycle.MutableLiveData
+                import some.other.pkg.SomeData
+
+                fun foo() {
+                    val liveData = MutableLiveData<SomeData>()
+                    val x = SomeData()
+                    liveData.value = x
+                    liveData.postValue(bar(6))
+                }
+
+                fun bar(x: Int): SomeData {
+                    return SomeData(extras = x)
+                }
+            """
+            ).indented(),
+            DATA_LIB,
+        ).expectClean()
+    }
+
+    @Test
+    fun dataClassFromBinary_nullable() {
+        check(
+            kotlin(
+                """
+                package com.example
+
+                import androidx.lifecycle.MutableLiveData
+                import some.other.pkg.SomeData
+
+                fun foo() {
+                    val liveData = MutableLiveData<SomeData>()
+                    val bar: SomeData? = SomeData()
+                    liveData.value = bar
+                }
+            """
+            ).indented(),
+            DATA_LIB,
+        ).expect(
+            """
+src/com/example/test.kt:9: Error: Expected non-nullable value [NullSafeMutableLiveData]
+    liveData.value = bar
+                     ~~~
+1 errors, 0 warnings
+        """
+        ).expectFixDiffs(
+            """
+Fix for src/com/example/test.kt line 9: Change `LiveData` type to nullable:
+@@ -7 +7
+-     val liveData = MutableLiveData<SomeData>()
++     val liveData = MutableLiveData<SomeData?>()
+Fix for src/com/example/test.kt line 9: Add non-null asserted (!!) call:
+@@ -9 +9
+-     liveData.value = bar
++     liveData.value = bar!!
+        """
+        )
+    }
+
+    @Test
+    fun typeArgumentFromJava() {
+        check(
+            kotlin(
+                """
+                package com.example
+
+                import androidx.lifecycle.LiveData
+                import some.other.pkg.SomeData
+
+                abstract class Test : LiveData<SomeData>() {
+                  abstract val remoteRefreshCounter: RemoteRefreshCounter
+
+                  fun foo() {
+                    val counterValue = remoteRefreshCounter.value
+                    // This will trigger the detector, but the receiver type is from Java.
+                    remoteRefreshCounter.value = (counterValue ?: 1) - 1
+                  }
+                }
+                """
+            ).indented(),
+            DATA_LIB,
+            java(
+                """
+                package com.example;
+
+                import androidx.lifecycle.MutableLiveData;
+
+                public final class RemoteRefreshCounter extends MutableLiveData<Integer> {
+                  public RemoteRefreshCounter() {}
+                }
+                """
+            ).indented(),
+        ).expectClean()
+    }
+
+    private companion object {
+        val DATA_LIB: TestFile =
+            bytecode(
+                "libs/data.jar",
+                kotlin(
+                    """
+                    package some.other.pkg
+
+                    data class SomeData
+                    @JvmOverloads
+                    constructor(val items: List<Boolean> = listOf(), val extras: Int = 42)
+                """
+                ).indented(),
+                0x9ae81803,
+                """
+                META-INF/main.kotlin_module:
+                H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgEuXiTs7P1UutSMwtyEkVYgtJLS7x
+                LlFi0GIAAJY6UNwvAAAA
+                """,
+                """
+                some/other/pkg/SomeData.class:
+                H4sIAAAAAAAA/41X3VMTVxT/3c3XZgmwiSDIh6JoDQkaRFrbgl+g1mBQC4pa
+                bOsSVlhIdnF3Q/Wl45N/gjPtS2f60Cce6kwFp850qL71L+pDp9NzdpcACVJm
+                wr3n3j3nd8/H79w7/PXv738AGMIzgTbHKus5y13Q7dzy0nxuipZXNFeLQQio
+                i9qKlitp5nzu1uyiXnRjCAlERwzTcC8ItKQLnkLFNUq5guG4w/m+aYG+2u2R
+                whbOqGWVdM0cvuDrHiosWW7JMHOLK+Xc+Er51opulyxtzhkW6C1Y9nxuUXdn
+                bc0wnZxmmparuYZF8k3LvVkplUgrYrh62ZGhCBzehmWYrm6bWimXN12brI2i
+                E0NCoLW4oBeXAvPbmq2VdVIUOJku1AY7vG1nikHmh/umE2hCs4JGqAKhNK8j
+                SCkI4wClqzYdCShojUPCQcqa/tS1NUdA5BNoxyHe7hAIuwuGw3n4QB0owrH6
+                NOcLu4V6RX+sVUruGCXItStF17InNHtJt4c50z2BRdEqlSg4L4tjW/INqu0R
+                gbheXnaf8SECqXRffUBHcUxBD3o57qhCMZwQkOd1N89V4DTW2uxefDqJjK4G
+                KaFE5gWUolVetkzddM9sXwxSkorWMnG1fxe+7ZG3y/vl4R4YCT75+JyfWIH7
+                6Q/q7lKjOkJ9+KQETuE0p5OilV3Lpxt3WF89Calpa/dGK0ZpTrdj+ETBOaZi
+                8yZy2muP8zI+Iwpqy8u6OSdwKr0Lteu2AlDybRgjjHu+xrIa1V6WF9nyEtW7
+                v8fvAPJlVKArnd/b7grbXWXFsb0Vv2DF6wm6zj5maZwSuKA5C2PWnJ5AARlu
+                zgmB5BYGXQn6PGfrFlGL/Miz3pcKbmOSkmTrjlfrqP6kopWIna27xfyVwPG9
+                7ie6XbTZkk7FkjVbv8pQ/3vJVLETuI8HfMnQKRGPLdSN9YoCB+o6gls9vT/i
+                +5fXYWbdI4ET++zb5ObVM6G72pzfJVJ5JUQPiuAhzgPomlui/acGrwZImqOe
+                /nvjeY8itUuKpG48V+gnqbRWG3xRViQ5QXOT/O6F3L7xfFAaEKNH5ViqSZZU
+                qUNOhVPSQHgglJJpHemQBqLvfo5Kamy8WU1sfb3+/kVoPK428ndP7lWbSCa4
+                GpjBKgypifEDajPJ4UFZVTvC7WJAXH//koGSvsZLQXKK5AMsTyarDsjkfEdY
+                ltX4ZNt2+K1PitrA8VNjU1YaNvvy9BJRLD5lzJuaW7F1gc7JiukaZT1vrhiO
+                QdS5vEUnoinTmdq6YJj6zUp5VrfvML2YFlZRK01rtsHrYPN4LVb1odsB2jjl
+                asWlCW05MFOmrIpd1K8ZvDgUYEzXeYMzxJgw1VVCih84mpe91Sye0BylMOM0
+                p/iVo7mR1vRWkIVNq3ukx5xozaYa3iCZWUdLJvsabZmu1+h85cE4NDIpo6So
+                kKlL6x7fCF3oZnqRxAcLT+JjNl2IVV2Qaa4wH6XAH36xie60Yj8uEWCE5o7u
+                8Pc/IPaGurk7QlJUzZzPZLvW8ZHvzAqNYUiKXHULZMduJXESxwiF0XJMeP6S
+                +Q1tv1ajiPqbXgSJQPYj8L1N70hYEn3IBO5tB+xc3QdgEtmqN7213oh9e9Bf
+                9aC31oPdQJL8bAWVHaWZi6T6lX0L6YGXxy0H/CKq1SKq1SKqO4qY2+FUTRHj
+                GCAC+j6eDYrY7RVR5nD9KrLYyXVcw1n//KCODQrJ9FLQKQxwDZJ3fvotzj1Y
+                x6epz9dwgWHWcDk15suda7im9q0hv4YbtclMB7Fs91fwoxM4eJHwmTFtPubN
+                kU71yAJjvsHUo5FtaT1IajKacYfSwMBtAbDAXUwHzpaD1hnKZH9BJLya/RPS
+                j4iEVrMbkCb4hH76e4MZb5NPoXXnT7wIr1bJ3IBwTP4HLTFIierR/M/AZlWG
+                8BBf08Gc9xhz4J7nRwTfBIU+RzKbNActnA1FxDq0VzuCoc9VxOagzn51v61r
+                Ue+UeHBKKoj2JMmciXhGhKI78f3cx2tyL3vo33mjhac0z9NukWDmZhDKQ8/j
+                cZ72FkiEkccilmYgHJRQnkGLg24HdLcp3ki/dm+MOuhzkHFw0sExB1kH/Q5O
+                Objr4KGDgoMhUvsPdw4FK1ANAAA=
+                """
+            )
+    }
 }
diff --git a/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/stubs/Stubs.kt b/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/stubs/Stubs.kt
index dbd5ed0..96d15cc 100644
--- a/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/stubs/Stubs.kt
+++ b/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/stubs/Stubs.kt
@@ -25,7 +25,7 @@
     public abstract class LiveData<T> {
         protected void postValue(T value) {}
         protected void setValue(T value) {}
-        public T getValue() {}
+        public T getValue() { return null; }
     }
 """
 ).indented()
diff --git a/lifecycle/lifecycle-livedata-core/api/current.txt b/lifecycle/lifecycle-livedata-core/api/current.txt
index 1168cc2..810396e 100644
--- a/lifecycle/lifecycle-livedata-core/api/current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/current.txt
@@ -22,7 +22,7 @@
     method @Deprecated @MainThread public static inline <T> androidx.lifecycle.Observer<T> observe(androidx.lifecycle.LiveData<T>, androidx.lifecycle.LifecycleOwner owner, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onChanged);
   }
 
-  public class MutableLiveData<T> extends androidx.lifecycle.LiveData<T> {
+  public class MutableLiveData<T> extends androidx.lifecycle.LiveData<T!> {
     ctor public MutableLiveData();
     ctor public MutableLiveData(T!);
     method public void postValue(T!);
diff --git a/lifecycle/lifecycle-livedata-core/api/restricted_current.txt b/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
index 1168cc2..810396e 100644
--- a/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
@@ -22,7 +22,7 @@
     method @Deprecated @MainThread public static inline <T> androidx.lifecycle.Observer<T> observe(androidx.lifecycle.LiveData<T>, androidx.lifecycle.LifecycleOwner owner, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onChanged);
   }
 
-  public class MutableLiveData<T> extends androidx.lifecycle.LiveData<T> {
+  public class MutableLiveData<T> extends androidx.lifecycle.LiveData<T!> {
     ctor public MutableLiveData();
     ctor public MutableLiveData(T!);
     method public void postValue(T!);
diff --git a/lifecycle/lifecycle-livedata/api/current.txt b/lifecycle/lifecycle-livedata/api/current.txt
index 55bd162..3f39eb4 100644
--- a/lifecycle/lifecycle-livedata/api/current.txt
+++ b/lifecycle/lifecycle-livedata/api/current.txt
@@ -24,7 +24,7 @@
     property public abstract T? latestValue;
   }
 
-  public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T> {
+  public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T!> {
     ctor public MediatorLiveData();
     ctor public MediatorLiveData(T!);
     method @MainThread public <S> void addSource(androidx.lifecycle.LiveData<S!>, androidx.lifecycle.Observer<? super S!>);
diff --git a/lifecycle/lifecycle-livedata/api/restricted_current.txt b/lifecycle/lifecycle-livedata/api/restricted_current.txt
index 6724451..d22b8c4 100644
--- a/lifecycle/lifecycle-livedata/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata/api/restricted_current.txt
@@ -33,7 +33,7 @@
     property public abstract T? latestValue;
   }
 
-  public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T> {
+  public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T!> {
     ctor public MediatorLiveData();
     ctor public MediatorLiveData(T!);
     method @MainThread public <S> void addSource(androidx.lifecycle.LiveData<S!>, androidx.lifecycle.Observer<? super S!>);
diff --git a/lifecycle/lifecycle-runtime-lint/src/main/java/androidx/lifecycle/lint/RepeatOnLifecycleDetector.kt b/lifecycle/lifecycle-runtime-lint/src/main/java/androidx/lifecycle/lint/RepeatOnLifecycleDetector.kt
index 7d42aa1..a2192eb 100644
--- a/lifecycle/lifecycle-runtime-lint/src/main/java/androidx/lifecycle/lint/RepeatOnLifecycleDetector.kt
+++ b/lifecycle/lifecycle-runtime-lint/src/main/java/androidx/lifecycle/lint/RepeatOnLifecycleDetector.kt
@@ -59,7 +59,7 @@
     override fun applicableSuperClasses(): List<String>? = listOf(FRAGMENT_CLASS, ACTIVITY_CLASS)
 
     override fun visitClass(context: JavaContext, declaration: UClass) {
-        if (!isKotlin(context.psiFile)) return // Check only Kotlin files
+        if (!isKotlin(declaration.lang)) return // Check only Kotlin files
 
         val visitedMethods = mutableSetOf<PsiMethod>()
         declaration.methods.forEach { method ->
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInAppCompatActivityTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInAppCompatActivityTest.kt
index e5ee149..1ead559 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInAppCompatActivityTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInAppCompatActivityTest.kt
@@ -19,8 +19,8 @@
 import androidx.activity.compose.setContent
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import java.util.concurrent.CountDownLatch
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInComponentActivityTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInComponentActivityTest.kt
index 50f06f9..fb0e225 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInComponentActivityTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInComponentActivityTest.kt
@@ -19,8 +19,8 @@
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
index 3b02e84..4f8159f 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -30,6 +29,7 @@
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.ViewModelStore
 import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
 import androidx.lifecycle.viewmodel.CreationExtras
 import androidx.lifecycle.viewmodel.MutableCreationExtras
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
index e0a9e70..b638571 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
@@ -28,7 +28,8 @@
 
     override val issues = listOf(
         EagerConfigurationDetector.ISSUE,
-        InternalApiUsageDetector.ISSUE,
+        InternalApiUsageDetector.INTERNAL_GRADLE_ISSUE,
+        InternalApiUsageDetector.INTERNAL_AGP_ISSUE,
         WithPluginClasspathUsageDetector.ISSUE,
     )
 
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/InternalApiUsageDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/InternalApiUsageDetector.kt
index 5e1b881..5b1d9cf 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/InternalApiUsageDetector.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/InternalApiUsageDetector.kt
@@ -49,9 +49,14 @@
 
                 if (resolved is PsiClass) {
                     if (resolved.isInternalGradleApi()) {
-                        reportIncidentForNode(node, "Avoid using internal Gradle APIs")
+                        reportIncidentForNode(
+                            INTERNAL_GRADLE_ISSUE,
+                            node,
+                            "Avoid using internal Gradle APIs"
+                        )
                     } else if (resolved.isInternalAgpApi()) {
                         reportIncidentForNode(
+                            INTERNAL_AGP_ISSUE,
                             node,
                             "Avoid using internal Android Gradle Plugin APIs"
                         )
@@ -60,9 +65,9 @@
             }
         }
 
-        private fun reportIncidentForNode(node: UElement, message: String) {
+        private fun reportIncidentForNode(issue: Issue, node: UElement, message: String) {
             val incident = Incident(context)
-                .issue(ISSUE)
+                .issue(issue)
                 .location(context.getLocation(node))
                 .message(message)
                 .scope(node)
@@ -81,7 +86,7 @@
     }
 
     companion object {
-        val ISSUE = Issue.create(
+        val INTERNAL_GRADLE_ISSUE = Issue.create(
             "InternalGradleApiUsage",
             "Avoid using internal Gradle APIs",
             """
@@ -95,5 +100,19 @@
                 Scope.JAVA_FILE_SCOPE
             )
         )
+        val INTERNAL_AGP_ISSUE = Issue.create(
+            "InternalAgpApiUsage",
+            "Avoid using internal Android Gradle Plugin APIs",
+            """
+                Using internal APIs results in fragile plugin behavior as these types have no binary
+                compatibility guarantees. It is best to create a feature request to open up these
+                APIs if you find them useful.
+            """,
+            Category.CORRECTNESS, 5, Severity.ERROR,
+            Implementation(
+                InternalApiUsageDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
     }
 }
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/InternalApiUsageDetectorTest.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/InternalApiUsageDetectorTest.kt
index 23a8918..8f3a2e5 100644
--- a/lint/lint-gradle/src/test/java/androidx/lint/gradle/InternalApiUsageDetectorTest.kt
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/InternalApiUsageDetectorTest.kt
@@ -24,7 +24,10 @@
 @RunWith(JUnit4::class)
 class InternalApiUsageDetectorTest : GradleLintDetectorTest(
     detector = InternalApiUsageDetector(),
-    issues = listOf(InternalApiUsageDetector.ISSUE)
+    issues = listOf(
+        InternalApiUsageDetector.INTERNAL_GRADLE_ISSUE,
+        InternalApiUsageDetector.INTERNAL_AGP_ISSUE,
+    )
 ) {
     @Test
     fun `Test usage of internal Gradle API`() {
@@ -90,7 +93,7 @@
             .run()
             .expect(
                 """
-                src/test.kt:1: Error: Avoid using internal Android Gradle Plugin APIs [InternalGradleApiUsage]
+                src/test.kt:1: Error: Avoid using internal Android Gradle Plugin APIs [InternalAgpApiUsage]
                 import com.android.build.gradle.internal.lint.VariantInputs
                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                 1 errors, 0 warnings
diff --git a/loader/loader/api/current.txt b/loader/loader/api/current.txt
index 9b59aa7..9e811b6 100644
--- a/loader/loader/api/current.txt
+++ b/loader/loader/api/current.txt
@@ -24,7 +24,7 @@
 
 package androidx.loader.content {
 
-  public abstract class AsyncTaskLoader<D> extends androidx.loader.content.Loader<D> {
+  public abstract class AsyncTaskLoader<D> extends androidx.loader.content.Loader<D!> {
     ctor public AsyncTaskLoader(android.content.Context);
     method public void cancelLoadInBackground();
     method protected java.util.concurrent.Executor getExecutor();
@@ -35,7 +35,7 @@
     method public void setUpdateThrottle(long);
   }
 
-  public class CursorLoader extends androidx.loader.content.AsyncTaskLoader<android.database.Cursor> {
+  public class CursorLoader extends androidx.loader.content.AsyncTaskLoader<android.database.Cursor!> {
     ctor public CursorLoader(android.content.Context);
     ctor public CursorLoader(android.content.Context, android.net.Uri, String![]?, String?, String![]?, String?);
     method public void deliverResult(android.database.Cursor?);
diff --git a/loader/loader/api/restricted_current.txt b/loader/loader/api/restricted_current.txt
index 9b59aa7..9e811b6 100644
--- a/loader/loader/api/restricted_current.txt
+++ b/loader/loader/api/restricted_current.txt
@@ -24,7 +24,7 @@
 
 package androidx.loader.content {
 
-  public abstract class AsyncTaskLoader<D> extends androidx.loader.content.Loader<D> {
+  public abstract class AsyncTaskLoader<D> extends androidx.loader.content.Loader<D!> {
     ctor public AsyncTaskLoader(android.content.Context);
     method public void cancelLoadInBackground();
     method protected java.util.concurrent.Executor getExecutor();
@@ -35,7 +35,7 @@
     method public void setUpdateThrottle(long);
   }
 
-  public class CursorLoader extends androidx.loader.content.AsyncTaskLoader<android.database.Cursor> {
+  public class CursorLoader extends androidx.loader.content.AsyncTaskLoader<android.database.Cursor!> {
     ctor public CursorLoader(android.content.Context);
     ctor public CursorLoader(android.content.Context, android.net.Uri, String![]?, String?, String![]?, String?);
     method public void deliverResult(android.database.Cursor?);
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index 6e3a990..449e4b9 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -437,7 +437,7 @@
     property public String name;
   }
 
-  public static final class NavType.ParcelableArrayType<D extends android.os.Parcelable> extends androidx.navigation.NavType<D[]> {
+  public static final class NavType.ParcelableArrayType<D extends android.os.Parcelable> extends androidx.navigation.NavType<D[]?> {
     ctor public NavType.ParcelableArrayType(Class<D> type);
     method public D[]? get(android.os.Bundle bundle, String key);
     method public D[] parseValue(String value);
@@ -454,7 +454,7 @@
     property public String name;
   }
 
-  public static final class NavType.SerializableArrayType<D extends java.io.Serializable> extends androidx.navigation.NavType<D[]> {
+  public static final class NavType.SerializableArrayType<D extends java.io.Serializable> extends androidx.navigation.NavType<D[]?> {
     ctor public NavType.SerializableArrayType(Class<D> type);
     method public D[]? get(android.os.Bundle bundle, String key);
     method public D[] parseValue(String value);
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index 6e3a990..449e4b9 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -437,7 +437,7 @@
     property public String name;
   }
 
-  public static final class NavType.ParcelableArrayType<D extends android.os.Parcelable> extends androidx.navigation.NavType<D[]> {
+  public static final class NavType.ParcelableArrayType<D extends android.os.Parcelable> extends androidx.navigation.NavType<D[]?> {
     ctor public NavType.ParcelableArrayType(Class<D> type);
     method public D[]? get(android.os.Bundle bundle, String key);
     method public D[] parseValue(String value);
@@ -454,7 +454,7 @@
     property public String name;
   }
 
-  public static final class NavType.SerializableArrayType<D extends java.io.Serializable> extends androidx.navigation.NavType<D[]> {
+  public static final class NavType.SerializableArrayType<D extends java.io.Serializable> extends androidx.navigation.NavType<D[]?> {
     ctor public NavType.SerializableArrayType(Class<D> type);
     method public D[]? get(android.os.Bundle bundle, String key);
     method public D[] parseValue(String value);
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
index 9364be4..3c4a03b0 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
@@ -21,7 +21,12 @@
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
 import kotlin.test.assertFailsWith
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -60,6 +65,45 @@
     }
 
     @Test
+    fun navDestinationKClass() {
+        @Serializable
+        class TestClass
+
+        val destination = provider.navDestination(route = TestClass::class) { }
+        assertWithMessage("NavDestination should have route set")
+            .that(destination.route)
+            .isEqualTo(
+                "androidx.navigation.NavDestinationTest.navDestinationKClass.TestClass"
+            )
+        assertWithMessage("NavDestination should have id set")
+            .that(destination.id)
+            .isEqualTo(serializer<TestClass>().hashCode())
+    }
+
+    @Test
+    fun navDestinationKClassArguments() {
+        @Serializable
+        @SerialName(DESTINATION_ROUTE)
+        class TestClass(val arg: Int, val arg2: String = "123")
+
+        val destination = provider.navDestination(route = TestClass::class) { }
+        assertWithMessage("NavDestination should have route set")
+            .that(destination.route)
+            .isEqualTo(
+                "$DESTINATION_ROUTE/{arg}?arg2={arg2}"
+            )
+        assertWithMessage("NavDestination should have id set")
+            .that(destination.id)
+            .isEqualTo(serializer<TestClass>().hashCode())
+        assertWithMessage("NavDestination should have argument added")
+            .that(destination.arguments["arg"])
+            .isNotNull()
+        assertWithMessage("NavArgument should have default value added")
+            .that(destination.arguments["arg2"]?.isDefaultValuePresent)
+            .isTrue()
+    }
+
+    @Test
     fun navDestinationDefaultArguments() {
         val destination = provider.navDestination(DESTINATION_ID) {
             argument("testArg") {
@@ -189,3 +233,15 @@
     builder: NavDestinationBuilder<NavDestination>.() -> Unit
 ): NavDestination =
     NavDestinationBuilder(this[NoOpNavigator::class], route = route).apply(builder).build()
+
+/**
+ * Instead of constructing a NavGraph from the NavigatorProvider, construct
+ * a NavDestination directly to allow for testing NavDestinationBuilder in
+ * isolation.
+ */
+fun NavigatorProvider.navDestination(
+    route: KClass<*>,
+    typeMap: Map<KType, NavType<*>> = emptyMap(),
+    builder: NavDestinationBuilder<NavDestination>.() -> Unit
+): NavDestination =
+    NavDestinationBuilder(this[NoOpNavigator::class], route, typeMap).apply(builder).build()
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
index 69d1eda..c4e59ab 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
@@ -17,9 +17,14 @@
 package androidx.navigation
 
 import androidx.annotation.IdRes
+import androidx.navigation.serialization.generateRoutePattern
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.reflect.KClass
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -111,6 +116,74 @@
             .isTrue()
     }
 
+    @Test
+    fun navigationAddDestinationKClassBuilder() {
+        @Serializable
+        class TestClass
+
+        val serializer = serializer<TestClass>()
+        val route = serializer.generateRoutePattern()
+        val graph = provider.navigation(
+            startDestination = route
+        ) {
+            val builder = NavDestinationBuilder(provider[NoOpNavigator::class], TestClass::class)
+            addDestination(builder.build())
+        }
+        assertWithMessage("Destination route should be added to the graph")
+            .that(route in graph)
+            .isTrue()
+        assertWithMessage("Destination id should be added to the graph")
+            .that(serializer.hashCode() in graph)
+            .isTrue()
+    }
+
+    @Test
+    fun navigationAddDestinationWithArgsKClassBuilder() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        val serializer = serializer<TestClass>()
+        val route = serializer.generateRoutePattern()
+        val graph = provider.navigation(
+            startDestination = route
+        ) {
+            val builder = NavDestinationBuilder(provider[NoOpNavigator::class], TestClass::class)
+            addDestination(builder.build())
+        }
+        assertWithMessage("Destination route should be added to the graph")
+            .that(route in graph)
+            .isTrue()
+        assertWithMessage("Destination id should be added to the graph")
+            .that(serializer.hashCode() in graph)
+            .isTrue()
+    }
+
+    @Test fun navigationStartDestinationKClass() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        @Serializable
+        class Graph(val arg: Int)
+
+        val serializer = serializer<TestClass>()
+        val expected = serializer.generateRoutePattern()
+        val graph = provider.navigation(
+            route = Graph::class,
+            startDestination = TestClass::class
+        ) {
+            navDestination(TestClass::class) { }
+        }
+        assertWithMessage("Destination route should be added to the graph")
+            .that(expected in graph)
+            .isTrue()
+        assertWithMessage("startDestinationRoute should be set")
+            .that(graph.startDestinationRoute)
+            .isEqualTo(expected)
+        assertWithMessage("startDestinationId should be set")
+            .that(graph.startDestinationId)
+            .isEqualTo(serializer.hashCode())
+    }
+
     @Suppress("DEPRECATION")
     @Test(expected = IllegalStateException::class)
     fun navigationMissingStartDestination() {
@@ -152,6 +225,31 @@
             .that(DESTINATION_ROUTE in graph)
             .isTrue()
     }
+
+    @Test
+    fun navigationNestedKClass() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        @Serializable
+        class NestedGraph(val arg: Int)
+
+        val serializer = serializer<TestClass>()
+        val expected = serializer.generateRoutePattern()
+        val graph = provider.navigation(startDestination = NestedGraph::class) {
+            navigation(startDestination = TestClass::class, route = NestedGraph::class) {
+                navDestination(TestClass::class) {}
+            }
+        }
+        val nestedGraph = graph.findNode(
+            serializer<NestedGraph>().generateRoutePattern()
+        ) as NavGraph
+        assertThat(nestedGraph.startDestinationRoute).isEqualTo(expected)
+        assertThat(nestedGraph.startDestinationId).isEqualTo(serializer.hashCode())
+        assertWithMessage("Destination should be added to the nested graph")
+            .that(expected in nestedGraph)
+            .isTrue()
+    }
 }
 
 private const val DESTINATION_ID = 1
@@ -177,3 +275,12 @@
     route: String,
     builder: NavDestinationBuilder<NavDestination>.() -> Unit
 ) = destination(NavDestinationBuilder(provider[NoOpNavigator::class], route).apply(builder))
+
+/**
+ * Create a base NavDestination. Generally, only subtypes of NavDestination should be
+ * added to a NavGraph (hence why this is not in the common-ktx library)
+ */
+fun NavGraphBuilder.navDestination(
+    route: KClass<*>,
+    builder: NavDestinationBuilder<NavDestination>.() -> Unit
+) = destination(NavDestinationBuilder(provider[NoOpNavigator::class], route).apply(builder))
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
index 937c4c8..fafd8c0 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
@@ -18,7 +18,12 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.test.assertFailsWith
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
@@ -130,6 +135,36 @@
         val graph = NavGraph(navGraphNavigator)
         graph[DESTINATION_ID]
     }
+
+    @Test
+    fun graphSetStartDestinationRoute() {
+        @Serializable
+        @SerialName("route")
+        class TestClass(val arg: Int)
+
+        val graph = NavGraph(navGraphNavigator).apply {
+            setStartDestination(15)
+            addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
+        }
+        assertThat(graph.startDestinationId).isEqualTo(15)
+
+        graph.setStartDestination(TestClass::class)
+        assertThat(graph.startDestinationRoute).isEqualTo("route/{arg}")
+        assertThat(graph.startDestinationId).isEqualTo(serializer<TestClass>().hashCode())
+    }
+
+    @Test
+    fun graphSetStartDestinationRouteMissingStartDestination() {
+        @Serializable
+        class TestClass
+
+        val graph = NavGraph(navGraphNavigator)
+
+        // start destination not added via KClass, cannot match
+        assertFailsWith<IllegalStateException> {
+            graph.setStartDestination(TestClass::class)
+        }
+    }
 }
 
 private const val DESTINATION_ID = 1
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
index 237dd1f..61da16f 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
@@ -17,7 +17,14 @@
 package androidx.navigation
 
 import androidx.annotation.IdRes
+import androidx.annotation.RestrictTo
 import androidx.core.os.bundleOf
+import androidx.navigation.serialization.generateNavArguments
+import androidx.navigation.serialization.generateRoutePattern
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.serializer
 
 @DslMarker
 public annotation class NavDestinationDsl
@@ -72,6 +79,36 @@
         this(navigator, -1, route)
 
     /**
+     * DSL for constructing a new [NavDestination] with a serializable [KClass].
+     *
+     * This will also update the [id] of the destination based on KClass's serializer.
+     *
+     * @param navigator navigator used to create the destination
+     * @param route the [KClass] of the destination
+     * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom
+     * [NavType]. Required only when destination contains custom NavTypes.
+     *
+     * @return the newly constructed [NavDestination]
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @OptIn(InternalSerializationApi::class)
+    public constructor(
+        navigator: Navigator<out D>,
+        route: KClass<*>? = null,
+        typeMap: Map<KType, NavType<*>>? = null,
+    ) : this(
+        navigator,
+        route?.serializer()?.hashCode() ?: -1,
+        route?.serializer()?.generateRoutePattern(typeMap)
+    ) {
+        route?.apply {
+            serializer().generateNavArguments(typeMap).forEach {
+                arguments[it.name] = it.argument
+            }
+        }
+    }
+
+    /**
      * The descriptive label of the destination
      */
     public var label: CharSequence? = null
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index 1e1020e..b241168 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -26,6 +26,9 @@
 import androidx.core.content.res.use
 import androidx.navigation.common.R
 import java.lang.StringBuilder
+import kotlin.reflect.KClass
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.serializer
 
 /**
  * NavGraph is a collection of [NavDestination] nodes fetchable by ID.
@@ -189,11 +192,10 @@
 
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public fun findNode(route: String, searchParents: Boolean): NavDestination? {
-        // first try matching with routePattern
-        val id = createRoute(route).hashCode()
-        val destination = nodes[id] ?: nodes.valueIterator().asSequence().firstOrNull {
+        val destination = nodes.valueIterator().asSequence().firstOrNull {
+            // first try matching with routePattern
             // if not found with routePattern, try matching with route args
-            it.matchDeepLink(route) != null
+            it.route.equals(route) || it.matchDeepLink(route) != null
         }
 
         // Search the parent for the NavDestination if it is not a child of this navigation graph
@@ -330,6 +332,31 @@
     }
 
     /**
+     * Sets the starting destination for this NavGraph.
+     *
+     * This will override any previously set [startDestinationId]
+     *
+     * @param startDestRoute The route of the destination as a [KClass] to be shown when navigating
+     * to this NavGraph.
+     */
+    @OptIn(InternalSerializationApi::class)
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun setStartDestination(startDestRoute: KClass<*>) {
+        val serializer = startDestRoute.serializer()
+        val id = serializer.hashCode()
+        val startDest = findNode(id)
+        checkNotNull(startDest) {
+            "Cannot find startDestination $startDestRoute from NavGraph. Ensure the starting " +
+                "NavDestination was added via KClass."
+        }
+        // when dest id is based on serializer, we expect the dest route to have been generated
+        // and set
+        startDestinationRoute = startDest.route!!
+        // bypass startDestinationId setter so we don't set route back to null
+        this.startDestId = id
+    }
+
+    /**
      * The route for the starting destination for this NavGraph. When navigating to the
      * NavGraph, the destination represented by this route is the one the user will initially see.
      */
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
index d69f5d2..c19604c 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
@@ -17,6 +17,10 @@
 package androidx.navigation
 
 import androidx.annotation.IdRes
+import androidx.annotation.RestrictTo
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import kotlinx.serialization.InternalSerializationApi
 
 /**
  * Construct a new [NavGraph]
@@ -58,6 +62,26 @@
     .build()
 
 /**
+ * Construct a new [NavGraph]
+ *
+ * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
+ * respective NavDestination must be added as a [KClass] in order to match.
+ * @param route the graph's unique route as a [KClass]
+ * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+ * if [route] uses custom NavTypes.
+ *
+ * @return the newly constructed NavGraph
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public inline fun NavigatorProvider.navigation(
+    startDestination: KClass<*>,
+    route: KClass<*>? = null,
+    typeMap: Map<KType, NavType<*>>? = null,
+    builder: NavGraphBuilder.() -> Unit
+): NavGraph = NavGraphBuilder(this, startDestination, route, typeMap).apply(builder)
+    .build()
+
+/**
  * Construct a nested [NavGraph]
  *
  * @param id the destination's unique id
@@ -96,6 +120,25 @@
 ): Unit = destination(NavGraphBuilder(provider, startDestination, route).apply(builder))
 
 /**
+ * Construct a nested [NavGraph]
+ *
+ * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
+ * respective NavDestination must be added as a [KClass] in order to match.
+ * @param route the graph's unique route as a [KClass]
+ * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+ * if [route] uses custom NavTypes.
+ *
+ * @return the newly constructed nested NavGraph
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public inline fun NavGraphBuilder.navigation(
+    startDestination: KClass<*>,
+    route: KClass<*>,
+    typeMap: Map<KType, NavType<*>>? = null,
+    builder: NavGraphBuilder.() -> Unit
+): Unit = destination(NavGraphBuilder(provider, startDestination, route, typeMap).apply(builder))
+
+/**
  * DSL for constructing a new [NavGraph]
  */
 @NavDestinationDsl
@@ -106,6 +149,7 @@
     public val provider: NavigatorProvider
     @IdRes private var startDestinationId: Int = 0
     private var startDestinationRoute: String? = null
+    private var startDestinationClass: KClass<*>? = null
 
     /**
      * DSL for constructing a new [NavGraph]
@@ -151,6 +195,29 @@
         this.startDestinationRoute = startDestination
     }
 
+    /**
+     * DSL for constructing a new [NavGraph]
+     *
+     * @param provider navigator used to create the destination
+     * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
+     * respective NavDestination must be added as a [KClass] in order to match.
+     * @param route the graph's unique route as a [KClass]
+     * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+     * if [route] uses custom NavTypes.
+     *
+     * @return the newly created NavGraph
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public constructor(
+        provider: NavigatorProvider,
+        startDestination: KClass<*>,
+        route: KClass<*>?,
+        typeMap: Map<KType, NavType<*>>?
+    ) : super(provider[NavGraphNavigator::class], route, typeMap) {
+        this.provider = provider
+        this.startDestinationClass = startDestination
+    }
+
     private val destinations = mutableListOf<NavDestination>()
 
     /**
@@ -174,17 +241,23 @@
         destinations += destination
     }
 
+    @OptIn(InternalSerializationApi::class)
     override fun build(): NavGraph = super.build().also { navGraph ->
         navGraph.addDestinations(destinations)
-        if (startDestinationId == 0 && startDestinationRoute == null) {
+        if (startDestinationId == 0 && startDestinationRoute == null &&
+            startDestinationClass == null) {
             if (route != null) {
                 throw IllegalStateException("You must set a start destination route")
-            } else {
+            } else if (startDestinationId == 0) {
                 throw IllegalStateException("You must set a start destination id")
+            } else {
+                throw IllegalStateException("You must set a start destination KClass")
             }
         }
         if (startDestinationRoute != null) {
             navGraph.setStartDestination(startDestinationRoute!!)
+        } else if (startDestinationClass != null) {
+            navGraph.setStartDestination(startDestinationClass!!)
         } else {
             navGraph.setStartDestination(startDestinationId)
         }
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
index ae36260..d2b50f3e 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
@@ -42,12 +42,12 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color.Companion.LightGray
 import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.navigation.NavController
 import androidx.navigation.NavHostController
 import androidx.navigation.NavType
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavBackStackEntryProviderTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavBackStackEntryProviderTest.kt
index 20edc30..1e36d9d 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavBackStackEntryProviderTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavBackStackEntryProviderTest.kt
@@ -19,12 +19,12 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.saveable.rememberSaveableStateHolder
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.testing.TestNavigatorState
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
index bfbf4ee..32980f7 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
@@ -43,7 +43,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -54,6 +53,7 @@
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavBackStackEntryProvider.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavBackStackEntryProvider.kt
index 4957694..2ca5a6f 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavBackStackEntryProvider.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavBackStackEntryProvider.kt
@@ -19,10 +19,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.saveable.SaveableStateHolder
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
 import androidx.navigation.NavBackStackEntry
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 1b41906..a816657 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
@@ -44,7 +44,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.NavDestination
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
index 13c20d9..5769efe 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
@@ -504,12 +504,16 @@
             assertThat(fragmentNavigator.pendingOps.entries()).isEmpty()
         }
         // obs for upcoming pop
+        val countDownLatch1 = CountDownLatch(1)
         var pendingOps2: List<String> = emptyList()
         fragmentNavigator.pendingOps.onBackStackChangedStarted {
             pendingOps2 = it.entries().toList()
+            countDownLatch1.countDown()
         }
         // And then pop entry2
         fragmentNavigator.popBackStack(entry2, false)
+        // wait for onBackStackChangedStarted before asserting
+        assertThat(countDownLatch1.await(1000, TimeUnit.MILLISECONDS)).isTrue()
         assertThat(pendingOps2).containsExactly(entry1.id, entry2.id)
         activityRule.runOnUiThread {
             fragmentManager.executePendingTransactions()
@@ -524,17 +528,17 @@
 
         // Add an observer to ensure that we don't attempt to verify the state until animations
         // are complete and the viewLifecycle has been RESUMED.
-        val countDownLatch = CountDownLatch(1)
+        val countDownLatch2 = CountDownLatch(1)
         activityRule.runOnUiThread {
             fragment1?.viewLifecycleOwner?.lifecycle?.addObserver(object : LifecycleEventObserver {
                 override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                     if (event == Lifecycle.Event.ON_RESUME) {
-                        countDownLatch.countDown()
+                        countDownLatch2.countDown()
                     }
                 }
             })
         }
-        assertThat(countDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(countDownLatch2.await(1000, TimeUnit.MILLISECONDS)).isTrue()
         assertThat(navigatorState.backStack.value).containsExactly(entry1)
         // We should ensure the fragment manager is on the proper fragment at the end
         assertWithMessage("FragmentManager back stack should be empty")
@@ -1742,17 +1746,21 @@
         assertThat(pendingOps1).containsExactly(entry2.id, entry3.id)
         assertThat(fragmentNavigator.pendingOps.entries()).isEmpty()
         // obs for upcoming pop
+        val countDownLatch2 = CountDownLatch(1)
         var pendingOps2: List<String> = emptyList()
         fragmentNavigator.pendingOps.onBackStackChangedStarted {
             pendingOps2 = it.entries().toList()
+            countDownLatch2.countDown()
         }
         fragmentNavigator.popBackStack(entry2, false)
+        // wait for onBackStackChangedStarted before asserting
+        assertThat(countDownLatch2.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(pendingOps2).containsExactly(entry1.id, entry2.id)
         assertThat(navigatorState.backStack.value).containsExactly(entry1)
         activityRule.runOnUiThread {
             fragmentManager.executePendingTransactions()
             assertThat(fragmentNavigator.pendingOps.entries()).isEmpty()
         }
-        assertThat(pendingOps2).containsExactly(entry1.id, entry2.id)
         val fragment1 = fragmentManager.findFragmentById(R.id.container) as AnimatorFragment
         assertWithMessage("Fragment should be added")
             .that(fragment1)
@@ -1771,17 +1779,17 @@
 
         // Add an observer to ensure that we don't attempt to verify the state until animations
         // are complete and the viewLifecycle has been RESUMED.
-        val countDownLatch2 = CountDownLatch(1)
+        val countDownLatch3 = CountDownLatch(1)
         activityRule.runOnUiThread {
             fragment1.viewLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
                 override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                     if (event == Lifecycle.Event.ON_RESUME) {
-                        countDownLatch2.countDown()
+                        countDownLatch3.countDown()
                     }
                 }
             })
         }
-        assertThat(countDownLatch2.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(countDownLatch3.await(1000, TimeUnit.MILLISECONDS)).isTrue()
 
         // Entry 1 should move back to RESUMED
         assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index b356f96..e372b42 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -98,7 +98,7 @@
     field @Deprecated public final int requestedLoadSize;
   }
 
-  public final class ItemSnapshotList<T> extends kotlin.collections.AbstractList<T> {
+  public final class ItemSnapshotList<T> extends kotlin.collections.AbstractList<T?> {
     ctor public ItemSnapshotList(@IntRange(from=0L) int placeholdersBefore, @IntRange(from=0L) int placeholdersAfter, java.util.List<? extends T> items);
     method public T? get(int index);
     method public java.util.List<T> getItems();
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index b356f96..e372b42 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -98,7 +98,7 @@
     field @Deprecated public final int requestedLoadSize;
   }
 
-  public final class ItemSnapshotList<T> extends kotlin.collections.AbstractList<T> {
+  public final class ItemSnapshotList<T> extends kotlin.collections.AbstractList<T?> {
     ctor public ItemSnapshotList(@IntRange(from=0L) int placeholdersBefore, @IntRange(from=0L) int placeholdersAfter, java.util.List<? extends T> items);
     method public T? get(int index);
     method public java.util.List<T> getItems();
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index cd7c06c..aa42ab4 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -26,5 +26,5 @@
 # Disable docs
 androidx.enableDocumentation=false
 androidx.playground.snapshotBuildId=11349412
-androidx.playground.metalavaBuildId=11569610
+androidx.playground.metalavaBuildId=11582635
 androidx.studio.type=playground
\ No newline at end of file
diff --git a/preference/preference/api/current.txt b/preference/preference/api/current.txt
index 3bbbb79..863b7b2 100644
--- a/preference/preference/api/current.txt
+++ b/preference/preference/api/current.txt
@@ -58,7 +58,7 @@
     method public void onBindEditText(android.widget.EditText);
   }
 
-  public static final class EditTextPreference.SimpleSummaryProvider implements androidx.preference.Preference.SummaryProvider<androidx.preference.EditTextPreference> {
+  public static final class EditTextPreference.SimpleSummaryProvider implements androidx.preference.Preference.SummaryProvider<androidx.preference.EditTextPreference!> {
     method public static androidx.preference.EditTextPreference.SimpleSummaryProvider getInstance();
     method public CharSequence? provideSummary(androidx.preference.EditTextPreference);
   }
@@ -94,7 +94,7 @@
     method public void setValueIndex(int);
   }
 
-  public static final class ListPreference.SimpleSummaryProvider implements androidx.preference.Preference.SummaryProvider<androidx.preference.ListPreference> {
+  public static final class ListPreference.SimpleSummaryProvider implements androidx.preference.Preference.SummaryProvider<androidx.preference.ListPreference!> {
     method public static androidx.preference.ListPreference.SimpleSummaryProvider getInstance();
     method public CharSequence? provideSummary(androidx.preference.ListPreference);
   }
@@ -142,7 +142,7 @@
     method public void onDialogClosed(boolean);
   }
 
-  public class Preference implements java.lang.Comparable<androidx.preference.Preference> {
+  public class Preference implements java.lang.Comparable<androidx.preference.Preference!> {
     ctor public Preference(android.content.Context);
     ctor public Preference(android.content.Context, android.util.AttributeSet?);
     ctor public Preference(android.content.Context, android.util.AttributeSet?, int);
diff --git a/preference/preference/api/restricted_current.txt b/preference/preference/api/restricted_current.txt
index 133fb62..c7a186b 100644
--- a/preference/preference/api/restricted_current.txt
+++ b/preference/preference/api/restricted_current.txt
@@ -58,7 +58,7 @@
     method public void onBindEditText(android.widget.EditText);
   }
 
-  public static final class EditTextPreference.SimpleSummaryProvider implements androidx.preference.Preference.SummaryProvider<androidx.preference.EditTextPreference> {
+  public static final class EditTextPreference.SimpleSummaryProvider implements androidx.preference.Preference.SummaryProvider<androidx.preference.EditTextPreference!> {
     method public static androidx.preference.EditTextPreference.SimpleSummaryProvider getInstance();
     method public CharSequence? provideSummary(androidx.preference.EditTextPreference);
   }
@@ -94,7 +94,7 @@
     method public void setValueIndex(int);
   }
 
-  public static final class ListPreference.SimpleSummaryProvider implements androidx.preference.Preference.SummaryProvider<androidx.preference.ListPreference> {
+  public static final class ListPreference.SimpleSummaryProvider implements androidx.preference.Preference.SummaryProvider<androidx.preference.ListPreference!> {
     method public static androidx.preference.ListPreference.SimpleSummaryProvider getInstance();
     method public CharSequence? provideSummary(androidx.preference.ListPreference);
   }
@@ -142,7 +142,7 @@
     method public void onDialogClosed(boolean);
   }
 
-  public class Preference implements java.lang.Comparable<androidx.preference.Preference> {
+  public class Preference implements java.lang.Comparable<androidx.preference.Preference!> {
     ctor public Preference(android.content.Context);
     ctor public Preference(android.content.Context, android.util.AttributeSet?);
     ctor public Preference(android.content.Context, android.util.AttributeSet?, int);
@@ -426,7 +426,7 @@
     method public int getPreferenceAdapterPosition(String);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PreferenceGroupAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.preference.PreferenceViewHolder> implements androidx.preference.PreferenceGroup.PreferencePositionCallback {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PreferenceGroupAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.preference.PreferenceViewHolder!> implements androidx.preference.PreferenceGroup.PreferencePositionCallback {
     ctor public PreferenceGroupAdapter(androidx.preference.PreferenceGroup);
     method public androidx.preference.Preference? getItem(int);
     method public int getItemCount();
diff --git a/preference/preference/res/values-sk/strings.xml b/preference/preference/res/values-sk/strings.xml
index 5b535d01..6c7dff3 100644
--- a/preference/preference/res/values-sk/strings.xml
+++ b/preference/preference/res/values-sk/strings.xml
@@ -5,7 +5,7 @@
     <string name="v7_preference_off" msgid="3140233346420563315">"Vypnuté"</string>
     <string name="expand_button_title" msgid="2427401033573778270">"Rozšírené"</string>
     <string name="summary_collapsed_preference_list" msgid="9167775378838880170">"<xliff:g id="CURRENT_ITEMS">%1$s</xliff:g>, <xliff:g id="ADDED_ITEMS">%2$s</xliff:g>"</string>
-    <string name="copy" msgid="6083905920877235314">"KopírovaÅ¥"</string>
+    <string name="copy" msgid="6083905920877235314">"SkopírovaÅ¥"</string>
     <string name="preference_copied" msgid="6685851473431805375">"Položka <xliff:g id="SUMMARY">%1$s</xliff:g> bola skopírovaná do schránky."</string>
     <string name="not_set" msgid="6573031135582639649">"Nenastavené"</string>
 </resources>
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
index 83c3fbe..112d751 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
@@ -17,16 +17,10 @@
 
 import android.content.Context
 import android.os.Build
-import android.os.Bundle
-import android.os.IBinder
 import androidx.privacysandbox.sdkruntime.client.TestSdkConfigs
 import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
-import androidx.privacysandbox.sdkruntime.core.AppOwnedSdkSandboxInterfaceCompat
 import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
 import androidx.privacysandbox.sdkruntime.core.Versions
-import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
-import androidx.privacysandbox.sdkruntime.core.controller.LoadSdkCallback
 import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +29,7 @@
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
 import java.io.File
-import java.util.concurrent.Executor
+import java.lang.reflect.Proxy
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -157,45 +151,18 @@
     }
 
     private object NoOpFactory : SdkLoader.ControllerFactory {
-        override fun createControllerFor(sdkConfig: LocalSdkConfig) = NoOpImpl()
-    }
 
-    private class NoOpImpl : SdkSandboxControllerCompat.SandboxControllerImpl {
+        val controllerImplClass = SdkSandboxControllerCompat.SandboxControllerImpl::class.java
 
-        override fun loadSdk(
-            sdkName: String,
-            params: Bundle,
-            executor: Executor,
-            callback: LoadSdkCallback
-        ) {
-            executor.execute {
-                callback.onError(
-                    LoadSdkCompatException(
-                        LoadSdkCompatException.LOAD_SDK_INTERNAL_ERROR,
-                        "NoOp"
-                    )
-                )
-            }
-        }
+        val noOpProxy = Proxy.newProxyInstance(
+            controllerImplClass.classLoader,
+            arrayOf(controllerImplClass)
+        ) { proxy, method, args ->
+            throw UnsupportedOperationException(
+                "Unexpected method call (NoOp) object:$proxy, method: $method, args: $args"
+            )
+        } as SdkSandboxControllerCompat.SandboxControllerImpl
 
-        override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
-            throw UnsupportedOperationException("NoOp")
-        }
-
-        override fun getAppOwnedSdkSandboxInterfaces(): List<AppOwnedSdkSandboxInterfaceCompat> {
-            throw UnsupportedOperationException("NoOp")
-        }
-
-        override fun registerSdkSandboxActivityHandler(
-            handlerCompat: SdkSandboxActivityHandlerCompat
-        ): IBinder {
-            throw UnsupportedOperationException("NoOp")
-        }
-
-        override fun unregisterSdkSandboxActivityHandler(
-            handlerCompat: SdkSandboxActivityHandlerCompat
-        ) {
-            throw UnsupportedOperationException("NoOp")
-        }
+        override fun createControllerFor(sdkConfig: LocalSdkConfig) = noOpProxy
     }
 }
diff --git a/profileinstaller/profileinstaller/api/current.txt b/profileinstaller/profileinstaller/api/current.txt
index 52d8a3c..474d213 100644
--- a/profileinstaller/profileinstaller/api/current.txt
+++ b/profileinstaller/profileinstaller/api/current.txt
@@ -41,7 +41,7 @@
     method public void onResultReceived(int, Object?);
   }
 
-  public class ProfileInstallerInitializer implements androidx.startup.Initializer<androidx.profileinstaller.ProfileInstallerInitializer.Result> {
+  public class ProfileInstallerInitializer implements androidx.startup.Initializer<androidx.profileinstaller.ProfileInstallerInitializer.Result!> {
     ctor public ProfileInstallerInitializer();
     method public androidx.profileinstaller.ProfileInstallerInitializer.Result create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>!>!> dependencies();
diff --git a/profileinstaller/profileinstaller/api/restricted_current.txt b/profileinstaller/profileinstaller/api/restricted_current.txt
index 52d8a3c..474d213 100644
--- a/profileinstaller/profileinstaller/api/restricted_current.txt
+++ b/profileinstaller/profileinstaller/api/restricted_current.txt
@@ -41,7 +41,7 @@
     method public void onResultReceived(int, Object?);
   }
 
-  public class ProfileInstallerInitializer implements androidx.startup.Initializer<androidx.profileinstaller.ProfileInstallerInitializer.Result> {
+  public class ProfileInstallerInitializer implements androidx.startup.Initializer<androidx.profileinstaller.ProfileInstallerInitializer.Result!> {
     ctor public ProfileInstallerInitializer();
     method public androidx.profileinstaller.ProfileInstallerInitializer.Result create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>!>!> dependencies();
diff --git a/recyclerview/recyclerview-selection/api/current.txt b/recyclerview/recyclerview-selection/api/current.txt
index 81ea8cd..3fba41b 100644
--- a/recyclerview/recyclerview-selection/api/current.txt
+++ b/recyclerview/recyclerview-selection/api/current.txt
@@ -49,7 +49,7 @@
   @IntDef({androidx.recyclerview.selection.ItemKeyProvider.SCOPE_MAPPED, androidx.recyclerview.selection.ItemKeyProvider.SCOPE_CACHED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ItemKeyProvider.Scope {
   }
 
-  public final class MutableSelection<K> extends androidx.recyclerview.selection.Selection<K> {
+  public final class MutableSelection<K> extends androidx.recyclerview.selection.Selection<K!> {
     ctor public MutableSelection();
     method public boolean add(K);
     method public void clear();
@@ -80,7 +80,7 @@
     method public void onChanged();
   }
 
-  public class Selection<K> implements java.lang.Iterable<K> {
+  public class Selection<K> implements java.lang.Iterable<K!> {
     method public boolean contains(K?);
     method public boolean isEmpty();
     method public java.util.Iterator<K!> iterator();
@@ -139,7 +139,7 @@
     method public abstract boolean canSetStateForKey(K, boolean);
   }
 
-  public final class StableIdKeyProvider extends androidx.recyclerview.selection.ItemKeyProvider<java.lang.Long> {
+  public final class StableIdKeyProvider extends androidx.recyclerview.selection.ItemKeyProvider<java.lang.Long!> {
     ctor public StableIdKeyProvider(androidx.recyclerview.widget.RecyclerView);
     method public Long? getKey(int);
     method public int getPosition(Long);
diff --git a/recyclerview/recyclerview-selection/api/restricted_current.txt b/recyclerview/recyclerview-selection/api/restricted_current.txt
index 81ea8cd..3fba41b 100644
--- a/recyclerview/recyclerview-selection/api/restricted_current.txt
+++ b/recyclerview/recyclerview-selection/api/restricted_current.txt
@@ -49,7 +49,7 @@
   @IntDef({androidx.recyclerview.selection.ItemKeyProvider.SCOPE_MAPPED, androidx.recyclerview.selection.ItemKeyProvider.SCOPE_CACHED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ItemKeyProvider.Scope {
   }
 
-  public final class MutableSelection<K> extends androidx.recyclerview.selection.Selection<K> {
+  public final class MutableSelection<K> extends androidx.recyclerview.selection.Selection<K!> {
     ctor public MutableSelection();
     method public boolean add(K);
     method public void clear();
@@ -80,7 +80,7 @@
     method public void onChanged();
   }
 
-  public class Selection<K> implements java.lang.Iterable<K> {
+  public class Selection<K> implements java.lang.Iterable<K!> {
     method public boolean contains(K?);
     method public boolean isEmpty();
     method public java.util.Iterator<K!> iterator();
@@ -139,7 +139,7 @@
     method public abstract boolean canSetStateForKey(K, boolean);
   }
 
-  public final class StableIdKeyProvider extends androidx.recyclerview.selection.ItemKeyProvider<java.lang.Long> {
+  public final class StableIdKeyProvider extends androidx.recyclerview.selection.ItemKeyProvider<java.lang.Long!> {
     ctor public StableIdKeyProvider(androidx.recyclerview.widget.RecyclerView);
     method public Long? getKey(int);
     method public int getPosition(Long);
diff --git a/recyclerview/recyclerview/api/current.txt b/recyclerview/recyclerview/api/current.txt
index 866a48d..a204e06 100644
--- a/recyclerview/recyclerview/api/current.txt
+++ b/recyclerview/recyclerview/api/current.txt
@@ -70,7 +70,7 @@
     method public void onRemoved(int, int);
   }
 
-  public final class ConcatAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder> {
+  public final class ConcatAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder!> {
     ctor @java.lang.SafeVarargs public ConcatAdapter(androidx.recyclerview.widget.ConcatAdapter.Config, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder!>!...);
     ctor public ConcatAdapter(androidx.recyclerview.widget.ConcatAdapter.Config, java.util.List<? extends androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder!>!>);
     ctor @java.lang.SafeVarargs public ConcatAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder!>!...);
@@ -343,7 +343,7 @@
     method public int findTargetSnapPosition(androidx.recyclerview.widget.RecyclerView.LayoutManager!, int, int);
   }
 
-  public abstract class ListAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
+  public abstract class ListAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH!> {
     ctor protected ListAdapter(androidx.recyclerview.widget.AsyncDifferConfig<T!>);
     ctor protected ListAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T!>);
     method public java.util.List<T!> getCurrentList();
@@ -1023,7 +1023,7 @@
     field public static final int INVALID_POSITION = -1; // 0xffffffff
   }
 
-  public static class SortedList.BatchedCallback<T2> extends androidx.recyclerview.widget.SortedList.Callback<T2> {
+  public static class SortedList.BatchedCallback<T2> extends androidx.recyclerview.widget.SortedList.Callback<T2!> {
     ctor public SortedList.BatchedCallback(androidx.recyclerview.widget.SortedList.Callback<T2!>!);
     method public boolean areContentsTheSame(T2!, T2!);
     method public boolean areItemsTheSame(T2!, T2!);
@@ -1035,7 +1035,7 @@
     method public void onRemoved(int, int);
   }
 
-  public abstract static class SortedList.Callback<T2> implements java.util.Comparator<T2> androidx.recyclerview.widget.ListUpdateCallback {
+  public abstract static class SortedList.Callback<T2> implements java.util.Comparator<T2!> androidx.recyclerview.widget.ListUpdateCallback {
     ctor public SortedList.Callback();
     method public abstract boolean areContentsTheSame(T2!, T2!);
     method public abstract boolean areItemsTheSame(T2!, T2!);
@@ -1045,7 +1045,7 @@
     method public void onChanged(int, int, Object!);
   }
 
-  public abstract class SortedListAdapterCallback<T2> extends androidx.recyclerview.widget.SortedList.Callback<T2> {
+  public abstract class SortedListAdapterCallback<T2> extends androidx.recyclerview.widget.SortedList.Callback<T2!> {
     ctor public SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter<?>!);
     method public void onChanged(int, int);
     method public void onInserted(int, int);
diff --git a/recyclerview/recyclerview/api/restricted_current.txt b/recyclerview/recyclerview/api/restricted_current.txt
index bc0ed62..a48e767 100644
--- a/recyclerview/recyclerview/api/restricted_current.txt
+++ b/recyclerview/recyclerview/api/restricted_current.txt
@@ -70,7 +70,7 @@
     method public void onRemoved(int, int);
   }
 
-  public final class ConcatAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder> {
+  public final class ConcatAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder!> {
     ctor @java.lang.SafeVarargs public ConcatAdapter(androidx.recyclerview.widget.ConcatAdapter.Config, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder!>!...);
     ctor public ConcatAdapter(androidx.recyclerview.widget.ConcatAdapter.Config, java.util.List<? extends androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder!>!>);
     ctor @java.lang.SafeVarargs public ConcatAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder!>!...);
@@ -343,7 +343,7 @@
     method public int findTargetSnapPosition(androidx.recyclerview.widget.RecyclerView.LayoutManager!, int, int);
   }
 
-  public abstract class ListAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
+  public abstract class ListAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH!> {
     ctor protected ListAdapter(androidx.recyclerview.widget.AsyncDifferConfig<T!>);
     ctor protected ListAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T!>);
     method public java.util.List<T!> getCurrentList();
@@ -1026,7 +1026,7 @@
     field public static final int INVALID_POSITION = -1; // 0xffffffff
   }
 
-  public static class SortedList.BatchedCallback<T2> extends androidx.recyclerview.widget.SortedList.Callback<T2> {
+  public static class SortedList.BatchedCallback<T2> extends androidx.recyclerview.widget.SortedList.Callback<T2!> {
     ctor public SortedList.BatchedCallback(androidx.recyclerview.widget.SortedList.Callback<T2!>!);
     method public boolean areContentsTheSame(T2!, T2!);
     method public boolean areItemsTheSame(T2!, T2!);
@@ -1038,7 +1038,7 @@
     method public void onRemoved(int, int);
   }
 
-  public abstract static class SortedList.Callback<T2> implements java.util.Comparator<T2> androidx.recyclerview.widget.ListUpdateCallback {
+  public abstract static class SortedList.Callback<T2> implements java.util.Comparator<T2!> androidx.recyclerview.widget.ListUpdateCallback {
     ctor public SortedList.Callback();
     method public abstract boolean areContentsTheSame(T2!, T2!);
     method public abstract boolean areItemsTheSame(T2!, T2!);
@@ -1048,7 +1048,7 @@
     method public void onChanged(int, int, Object!);
   }
 
-  public abstract class SortedListAdapterCallback<T2> extends androidx.recyclerview.widget.SortedList.Callback<T2> {
+  public abstract class SortedListAdapterCallback<T2> extends androidx.recyclerview.widget.SortedList.Callback<T2!> {
     ctor public SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter<?>!);
     method public void onChanged(int, int);
     method public void onInserted(int, int);
diff --git a/remotecallback/remotecallback/api/current.txt b/remotecallback/remotecallback/api/current.txt
index 59631b3..771c360d 100644
--- a/remotecallback/remotecallback/api/current.txt
+++ b/remotecallback/remotecallback/api/current.txt
@@ -1,12 +1,12 @@
 // Signature format: 4.0
 package androidx.remotecallback {
 
-  public class AppWidgetProviderWithCallbacks<T extends androidx.remotecallback.CallbackReceiver> extends android.appwidget.AppWidgetProvider implements androidx.remotecallback.CallbackReceiver<T> {
+  public class AppWidgetProviderWithCallbacks<T extends androidx.remotecallback.CallbackReceiver> extends android.appwidget.AppWidgetProvider implements androidx.remotecallback.CallbackReceiver<T!> {
     ctor public AppWidgetProviderWithCallbacks();
     method public T createRemoteCallback(android.content.Context);
   }
 
-  public abstract class BroadcastReceiverWithCallbacks<T extends androidx.remotecallback.CallbackReceiver> extends android.content.BroadcastReceiver implements androidx.remotecallback.CallbackReceiver<T> {
+  public abstract class BroadcastReceiverWithCallbacks<T extends androidx.remotecallback.CallbackReceiver> extends android.content.BroadcastReceiver implements androidx.remotecallback.CallbackReceiver<T!> {
     ctor public BroadcastReceiverWithCallbacks();
     method public T createRemoteCallback(android.content.Context);
     method public void onReceive(android.content.Context!, android.content.Intent!);
@@ -29,7 +29,7 @@
     method public T createRemoteCallback(android.content.Context);
   }
 
-  public abstract class ContentProviderWithCallbacks<T extends androidx.remotecallback.ContentProviderWithCallbacks> extends android.content.ContentProvider implements androidx.remotecallback.CallbackReceiver<T> {
+  public abstract class ContentProviderWithCallbacks<T extends androidx.remotecallback.ContentProviderWithCallbacks> extends android.content.ContentProvider implements androidx.remotecallback.CallbackReceiver<T!> {
     ctor public ContentProviderWithCallbacks();
     method public T createRemoteCallback(android.content.Context);
   }
diff --git a/remotecallback/remotecallback/api/restricted_current.txt b/remotecallback/remotecallback/api/restricted_current.txt
index 766f121..353c81f 100644
--- a/remotecallback/remotecallback/api/restricted_current.txt
+++ b/remotecallback/remotecallback/api/restricted_current.txt
@@ -1,13 +1,13 @@
 // Signature format: 4.0
 package androidx.remotecallback {
 
-  public class AppWidgetProviderWithCallbacks<T extends androidx.remotecallback.CallbackReceiver> extends android.appwidget.AppWidgetProvider implements androidx.remotecallback.CallbackBase<T> androidx.remotecallback.CallbackReceiver<T> {
+  public class AppWidgetProviderWithCallbacks<T extends androidx.remotecallback.CallbackReceiver> extends android.appwidget.AppWidgetProvider implements androidx.remotecallback.CallbackBase<T!> androidx.remotecallback.CallbackReceiver<T!> {
     ctor public AppWidgetProviderWithCallbacks();
     method public T createRemoteCallback(android.content.Context);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.remotecallback.RemoteCallback toRemoteCallback(Class<T!>, android.content.Context, String?, android.os.Bundle, String);
   }
 
-  public abstract class BroadcastReceiverWithCallbacks<T extends androidx.remotecallback.CallbackReceiver> extends android.content.BroadcastReceiver implements androidx.remotecallback.CallbackBase<T> androidx.remotecallback.CallbackReceiver<T> {
+  public abstract class BroadcastReceiverWithCallbacks<T extends androidx.remotecallback.CallbackReceiver> extends android.content.BroadcastReceiver implements androidx.remotecallback.CallbackBase<T!> androidx.remotecallback.CallbackReceiver<T!> {
     ctor public BroadcastReceiverWithCallbacks();
     method public T createRemoteCallback(android.content.Context);
     method public void onReceive(android.content.Context!, android.content.Intent!);
@@ -37,7 +37,7 @@
     method public T createRemoteCallback(android.content.Context);
   }
 
-  public abstract class ContentProviderWithCallbacks<T extends androidx.remotecallback.ContentProviderWithCallbacks> extends android.content.ContentProvider implements androidx.remotecallback.CallbackBase<T> androidx.remotecallback.CallbackReceiver<T> {
+  public abstract class ContentProviderWithCallbacks<T extends androidx.remotecallback.ContentProviderWithCallbacks> extends android.content.ContentProvider implements androidx.remotecallback.CallbackBase<T!> androidx.remotecallback.CallbackReceiver<T!> {
     ctor public ContentProviderWithCallbacks();
     method public T createRemoteCallback(android.content.Context);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.remotecallback.RemoteCallback toRemoteCallback(Class<T!>, android.content.Context, String?, android.os.Bundle, String);
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/InvalidationTrackerExt.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/InvalidationTrackerExt.kt
deleted file mode 100644
index b3406d5..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/InvalidationTrackerExt.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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 java.util.concurrent.TimeUnit
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.withTimeout
-
-/**
- * Polls [InvalidationTracker] until it sets its pending refresh flag to true.
- */
-suspend fun InvalidationTracker.awaitPendingRefresh() {
-    withTimeout(TimeUnit.SECONDS.toMillis(10)) {
-        while (true) {
-            if (pendingRefresh.get()) return@withTimeout
-            delay(50)
-        }
-    }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
index 936ba40..0d1f8bf 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
@@ -21,6 +21,7 @@
 import androidx.sqlite.SQLiteException
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
 import org.junit.Rule
 import org.junit.Test
@@ -31,6 +32,7 @@
  */
 @RunWith(AndroidJUnit4::class)
 @LargeTest
+@SdkSuppress(minSdkVersion = 22) // b/329236938
 class AutoMigrationTest {
     @JvmField
     @Rule
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
index 54b013b..116aea1 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
@@ -16,12 +16,14 @@
 
 package androidx.room.integration.kotlintestapp.migration
 
+import androidx.kruth.assertThrows
 import androidx.room.Room
-import androidx.room.RoomDatabase
+import androidx.room.integration.kotlintestapp.TestDatabase
 import androidx.room.migration.Migration
 import androidx.room.testing.MigrationTestHelper
 import androidx.room.util.TableInfo
 import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.driver.AndroidSQLiteDriver
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
 import java.io.FileNotFoundException
@@ -47,14 +49,12 @@
         const val TEST_DB = "migration-test"
     }
 
-    abstract class EmptyDb : RoomDatabase()
-
     @Test
     @Throws(IOException::class)
     fun giveBadResource() {
         val helper = MigrationTestHelper(
             InstrumentationRegistry.getInstrumentation(),
-            EmptyDb::class.java
+            TestDatabase::class.java
         )
         try {
             helper.createDatabase(TEST_DB, 1)
@@ -297,6 +297,56 @@
         }
     }
 
+    @Test
+    fun compatModeUsingWrongApis() {
+        assertThrows<IllegalStateException> {
+            helper.createDatabase(version = 1)
+        }.hasMessageThat().contains(
+            "MigrationTestHelper functionality returning a SQLiteConnection is not possible " +
+                "because a SupportSQLiteOpenHelper was provided during configuration (i.e. no " +
+                "SQLiteDriver was provided)."
+        )
+
+        assertThrows<IllegalStateException> {
+            helper.runMigrationsAndValidate(
+                version = 1,
+                migrations = emptyList()
+            )
+        }.hasMessageThat().contains(
+            "MigrationTestHelper functionality returning a SQLiteConnection is not possible " +
+                "because a SupportSQLiteOpenHelper was provided during configuration (i.e. no " +
+                "SQLiteDriver was provided)."
+        )
+    }
+
+    @Test
+    fun noCompatModeUsingWrongApis() {
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        val dbFile = instrumentation.targetContext.getDatabasePath("test.db")
+        val driverHelper = MigrationTestHelper(
+            instrumentation = instrumentation,
+            driver = AndroidSQLiteDriver(dbFile.path),
+            databaseClass = MigrationDbKotlin::class
+        )
+        assertThrows<IllegalStateException> {
+            driverHelper.createDatabase(name = "test.db", version = 1)
+        }.hasMessageThat().contains(
+            "MigrationTestHelper functionality returning a SupportSQLiteDatabase is not possible " +
+                "because a SQLiteDriver was provided during configuration."
+        )
+
+        assertThrows<IllegalStateException> {
+            driverHelper.runMigrationsAndValidate(
+                name = "test.db",
+                version = 1,
+                validateDroppedTables = false
+            )
+        }.hasMessageThat().contains(
+            "MigrationTestHelper functionality returning a SupportSQLiteDatabase is not possible " +
+                "because a SQLiteDriver was provided during configuration."
+        )
+    }
+
     internal val MIGRATION_1_2: Migration = object : Migration(1, 2) {
         override fun migrate(db: SupportSQLiteDatabase) {
             db.execSQL(
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/FlowQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/FlowQueryTest.kt
index 02d20ce..8d7ae93 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/FlowQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/FlowQueryTest.kt
@@ -187,6 +187,32 @@
     }
 
     @Test
+    fun receiveBooks_update_viaSupportDatabase() = runBlocking {
+        booksDao.addAuthors(TestUtil.AUTHOR_1)
+        booksDao.addPublishers(TestUtil.PUBLISHER)
+        booksDao.addBooks(TestUtil.BOOK_1)
+
+        val channel = booksDao.getBooksFlow().produceIn(this)
+
+        assertThat(channel.receive()).containsExactly(TestUtil.BOOK_1)
+
+        // Update table without going through Room's transaction APIs
+        database.openHelper.writableDatabase.execSQL(
+            "UPDATE Book SET salesCnt = 5 WHERE bookId = 'b1'"
+        )
+        // Ask for a refresh to occur, validating trigger is installed without going through Room's
+        // transaction APIs.
+        database.invalidationTracker.refreshVersionsAsync()
+        drain() // drain async invalidate
+        yield()
+
+        assertThat(channel.receive()).containsExactly(TestUtil.BOOK_1.copy(salesCnt = 5))
+        assertThat(channel.isEmpty).isTrue()
+
+        channel.cancel()
+    }
+
+    @Test
     fun receiveBooks_update_multipleChannels() = runBlocking {
         booksDao.addAuthors(TestUtil.AUTHOR_1)
         booksDao.addPublishers(TestUtil.PUBLISHER)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
index 5e743f5..d2a5a2b 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
@@ -23,7 +23,6 @@
 import androidx.room.InvalidationTracker
 import androidx.room.Room
 import androidx.room.RoomDatabase
-import androidx.room.awaitPendingRefresh
 import androidx.room.integration.kotlintestapp.testutil.ItemStore
 import androidx.room.integration.kotlintestapp.testutil.PagingDb
 import androidx.room.integration.kotlintestapp.testutil.PagingEntity
@@ -268,17 +267,17 @@
                     toIndex = 20 + CONFIG.initialLoadSize
                 )
             )
-            assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
+
             // now do some changes in the database but don't let change notifications go through
             // to the data source. it should not crash :)
-            queryExecutor.filterFunction = { runnable ->
-                runnable !== db.invalidationTracker.refreshRunnable
-            }
+             queryExecutor.filterFunction = {
+                 // TODO(b/): Avoid relying on function name, very brittle.
+                 !it.toString().contains("refreshInvalidationAsync")
+             }
+
             db.getDao().deleteItems(
                 items.subList(0, 60).map { it.id }
             )
-            // make sure invalidation requests a refresh
-            db.invalidationTracker.awaitPendingRefresh()
 
             // make sure we blocked the refresh runnable until after the exception generates a
             // new paging source
@@ -322,7 +321,6 @@
             // Runs the original invalidationTracker.refreshRunnable.
             // Note that the second initial load's call to mRefreshRunnable resets the flag to
             // false, so this mRefreshRunnable will not detect changes in the table anymore.
-            assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
             queryExecutor.executeAll()
 
             itemStore.awaitInitialLoad()
@@ -342,7 +340,7 @@
         }
     }
 
-    @Ignore // b/260592924
+    @FlakyTest(bugId = 260592924)
     @Test
     fun prependWithBlockingObserver() {
         val items = createItems(startId = 0, count = 90)
@@ -362,7 +360,7 @@
                 Thread.sleep(3_500)
             }
         }
-        db.invalidationTracker.addWeakObserver(
+        db.invalidationTracker.addObserver(
             blockingObserver
         )
 
@@ -378,7 +376,6 @@
                 // should load starting from initial Key = 20
                 initialItems
             )
-            assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
 
             db.getDao().deleteItems(
                 items.subList(0, 60).map { it.id }
@@ -427,17 +424,18 @@
                     toIndex = CONFIG.initialLoadSize
                 )
             )
-            assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
+
             // now do some changes in the database but don't let change notifications go through
             // to the data source. it should not crash :)
-            queryExecutor.filterFunction = { runnable ->
-                runnable !== db.invalidationTracker.refreshRunnable
+            queryExecutor.filterFunction = {
+                // TODO(b/): Avoid relying on function name, very brittle.
+                !it.toString().contains("refreshInvalidationAsync")
             }
+
             db.getDao().deleteItems(
                 items.subList(0, 80).map { it.id }
             )
-            // make sure invalidation requests a refresh
-            db.invalidationTracker.awaitPendingRefresh()
+
             // make sure we blocked the refresh runnable until after the exception generates a
             // new paging source
             queryExecutor.awaitDeferredSizeAtLeast(1)
@@ -480,7 +478,6 @@
             // Runs the original invalidationTracker.refreshRunnable.
             // Note that the second initial load's call to mRefreshRunnable resets the flag to
             // false, so this mRefreshRunnable will not detect changes in the table anymore.
-            assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
             queryExecutor.executeAll()
 
             itemStore.awaitInitialLoad()
diff --git a/room/integration-tests/multiplatformtestapp/build.gradle b/room/integration-tests/multiplatformtestapp/build.gradle
index dfd48c4..1368729 100644
--- a/room/integration-tests/multiplatformtestapp/build.gradle
+++ b/room/integration-tests/multiplatformtestapp/build.gradle
@@ -13,8 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import androidx.build.KmpPlatformsKt
+import org.apache.commons.io.FileUtils
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
+import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
     id("AndroidXPlugin")
@@ -37,6 +41,7 @@
             dependencies {
                 implementation(libs.kotlinStdlib)
                 implementation(project(":room:room-runtime"))
+                implementation(project(":room:room-testing"))
                 implementation(project(":sqlite:sqlite-bundled"))
                 implementation(project(":kruth:kruth"))
                 implementation(libs.kotlinTest)
@@ -60,10 +65,25 @@
         nativeTest {
             dependsOn(commonTest)
         }
+        nonIosNativeTest {
+            dependsOn(nativeTest)
+        }
+        if (KmpPlatformsKt.enableMac(project)) {
+            iosTest {
+                dependsOn(nativeTest)
+            }
+        }
         targets.all { target ->
             if (target.platformType == KotlinPlatformType.native) {
-                target.compilations["test"].defaultSourceSet {
-                    dependsOn(nativeTest)
+                def test = target.compilations["test"]
+                if (target.konanTarget.family == Family.IOS) {
+                    test.defaultSourceSet {
+                        dependsOn(iosTest)
+                    }
+                } else {
+                    test.defaultSourceSet {
+                        dependsOn(nonIosNativeTest)
+                    }
                 }
             }
         }
@@ -86,6 +106,23 @@
 
 android {
     namespace "androidx.room.integration.multiplatformtestapp"
+    // TODO(b/317909626): Should be configured by Room Gradle Plugin
+    sourceSets {
+        androidTest.assets.srcDirs += files("$projectDir/schemas-ksp".toString())
+    }
+}
+
+// Copy schema files to the iOS binary output test directory that will be part of the bundle's
+// resources and read with NSBundle. This needs to be replaced with a more proper mechanism.
+// TODO(b/317909626): Should be configured by Room Gradle Plugin
+tasks.withType(KotlinNativeLink).configureEach { task ->
+    if (name.contains("linkDebugTestIos")) {
+        def inputSchemaDir = layout.projectDirectory.dir("schemas-ksp").getAsFile()
+        def outputSchemaDir = new File(task.destinationDirectory.getAsFile().get(), "/schemas-ksp")
+        task.doLast {
+            FileUtils.copyDirectory(inputSchemaDir, outputSchemaDir)
+        }
+    }
 }
 
 room {
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
index 8d2caf0..e4b1895 100644
--- a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
@@ -20,19 +20,30 @@
 import androidx.kruth.assertThrows
 import androidx.room.Room
 import androidx.room.migration.Migration
+import androidx.room.testing.MigrationTestHelper
 import androidx.sqlite.SQLiteDriver
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.driver.bundled.BundledSQLiteDriver
-import androidx.sqlite.execSQL
 import androidx.test.platform.app.InstrumentationRegistry
 import kotlin.test.AfterTest
 import kotlin.test.BeforeTest
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+
 class AutoMigrationTest : BaseAutoMigrationTest() {
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val file = instrumentation.targetContext.getDatabasePath("test.db")
-    override val driver: SQLiteDriver = BundledSQLiteDriver(file.path)
+    private val driver: SQLiteDriver = BundledSQLiteDriver(file.path)
+
+    @get:Rule
+    val migrationTestHelper = MigrationTestHelper(
+        instrumentation = instrumentation,
+        driver = driver,
+        databaseClass = AutoMigrationDatabase::class
+    )
+
+    override fun getTestHelper() = migrationTestHelper
 
     override fun getRoomDatabase(): AutoMigrationDatabase {
         return Room.databaseBuilder<AutoMigrationDatabase>(
@@ -43,9 +54,8 @@
 
     @Test
     fun migrationWithWrongOverride() = runTest {
-        val connection = driver.open()
         // Create database in V1
-        connection.execSQL("PRAGMA user_version = 1")
+        val connection = migrationTestHelper.createDatabase(1)
         connection.close()
 
         // Auto migrate to V2
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
new file mode 100644
index 0000000..b4a0849
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.multiplatformtestapp.test
+
+import androidx.room.Room
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import androidx.test.platform.app.InstrumentationRegistry
+
+class InvalidationTest : BaseInvalidationTest() {
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    override fun getRoomDatabase(): SampleDatabase {
+        return Room.inMemoryDatabaseBuilder<SampleDatabase>(
+            context = instrumentation.targetContext,
+        ).setDriver(BundledSQLiteDriver(":memory:"))
+            .build()
+    }
+}
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseAutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseAutoMigrationTest.kt
index 436377d..f898ae5 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseAutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseAutoMigrationTest.kt
@@ -17,6 +17,7 @@
 package androidx.room.integration.multiplatformtestapp.test
 
 import androidx.kruth.assertThat
+import androidx.kruth.assertThrows
 import androidx.room.AutoMigration
 import androidx.room.ColumnInfo
 import androidx.room.Dao
@@ -27,37 +28,27 @@
 import androidx.room.Query
 import androidx.room.RoomDatabase
 import androidx.room.Update
-import androidx.sqlite.SQLiteDriver
-import androidx.sqlite.execSQL
+import androidx.room.testing.MigrationTestHelper
 import androidx.sqlite.use
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
 
 abstract class BaseAutoMigrationTest {
-    abstract val driver: SQLiteDriver
+    abstract fun getTestHelper(): MigrationTestHelper
     abstract fun getRoomDatabase(): AutoMigrationDatabase
 
     @Test
-    fun migrateFromV1ToV2() = runTest {
-        val connection = driver.open()
-        // Create database in V1
-        connection.execSQL("CREATE TABLE IF NOT EXISTS " +
-            "`AutoMigrationEntity` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))"
-        )
-        connection.execSQL("CREATE TABLE IF NOT EXISTS " +
-            "room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)"
-        )
-        connection.execSQL("INSERT OR REPLACE INTO " +
-            "room_master_table (id,identity_hash) VALUES(42, 'a917f82d955ea88cc98a551d197529c3')"
-        )
-        connection.execSQL("PRAGMA user_version = 1")
+    fun migrateFromV1ToLatest() = runTest {
+        val migrationTestHelper = getTestHelper()
+
+        // Create database V1
+        val connection = migrationTestHelper.createDatabase(1)
+        // Insert some data, we'll validate it is present after migration
         connection.prepare("INSERT INTO AutoMigrationEntity (pk) VALUES (?)").use {
             it.bindLong(1, 1)
             assertThat(it.step()).isFalse() // SQLITE_DONE
         }
-        connection.prepare(
-            "SELECT * FROM AutoMigrationEntity"
-        ).use { stmt ->
+        connection.prepare("SELECT * FROM AutoMigrationEntity").use { stmt ->
             assertThat(stmt.step()).isTrue()
             // Make sure that there is only 1 column in V1
             assertThat(stmt.getColumnCount()).isEqualTo(1)
@@ -67,7 +58,7 @@
         }
         connection.close()
 
-        // Auto migrate to V2
+        // Auto migrate to latest
         val dbVersion2 = getRoomDatabase()
         assertThat(dbVersion2.dao().update(AutoMigrationEntity(1, 5))).isEqualTo(1)
         assertThat(dbVersion2.dao().getSingleItem().pk).isEqualTo(1)
@@ -75,6 +66,54 @@
         dbVersion2.close()
     }
 
+    @Test
+    fun migrateFromV1ToV2() = runTest {
+        val migrationTestHelper = getTestHelper()
+
+        // Create database V1
+        val connection = migrationTestHelper.createDatabase(1)
+        // Insert some data, we'll validate it is present after migration
+        connection.prepare("INSERT INTO AutoMigrationEntity (pk) VALUES (?)").use {
+            it.bindLong(1, 1)
+            assertThat(it.step()).isFalse() // SQLITE_DONE
+        }
+        connection.close()
+
+        // Auto migrate to V2
+        migrationTestHelper.runMigrationsAndValidate(2)
+    }
+
+    @Test
+    fun misuseTestHelperAlreadyCreatedDatabase() {
+        val migrationTestHelper = getTestHelper()
+
+        // Create database V1
+        migrationTestHelper.createDatabase(1).close()
+
+        // When trying to create at V1 again, fail due to database file being already created.
+        assertThrows<IllegalStateException> {
+            migrationTestHelper.createDatabase(1)
+        }.hasMessageThat()
+            .contains("Creation of tables didn't occur while creating a new database.")
+
+        // If trying to create at V2, migration will try to run and fail.
+        assertThrows<IllegalStateException> {
+            migrationTestHelper.createDatabase(2)
+        }.hasMessageThat()
+            .contains("A migration from 1 to 2 was required but not found.")
+    }
+
+    @Test
+    fun misuseTestHelperMissingDatabaseForValidateMigrations() {
+        val migrationTestHelper = getTestHelper()
+
+        // Try to validate migrations, but fail due to no previous database created.
+        assertThrows<IllegalStateException> {
+            migrationTestHelper.runMigrationsAndValidate(2, emptyList())
+        }.hasMessageThat()
+            .contains("Creation of tables should never occur while validating migrations.")
+    }
+
     @Entity
     data class AutoMigrationEntity(
         @PrimaryKey
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseBuilderTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseBuilderTest.kt
index 7b346386..90e3311 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseBuilderTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseBuilderTest.kt
@@ -17,8 +17,10 @@
 package androidx.room.integration.multiplatformtestapp.test
 
 import androidx.kruth.assertThat
+import androidx.kruth.assertThrows
 import androidx.room.RoomDatabase
 import androidx.sqlite.SQLiteConnection
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
 
@@ -66,4 +68,12 @@
 
         db2.close()
     }
+
+    @Test
+    fun setCoroutineContextWithoutDispatcher() {
+        assertThrows<IllegalArgumentException> {
+            getRoomDatabaseBuilder().setQueryCoroutineContext(EmptyCoroutineContext)
+        }.hasMessageThat()
+            .contains("It is required that the coroutine context contain a dispatcher.")
+    }
 }
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseInvalidationTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseInvalidationTest.kt
new file mode 100644
index 0000000..0570831
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseInvalidationTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.multiplatformtestapp.test
+
+import androidx.kruth.assertThat
+import androidx.room.InvalidationTracker
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withTimeout
+
+abstract class BaseInvalidationTest {
+
+    private lateinit var db: SampleDatabase
+
+    abstract fun getRoomDatabase(): SampleDatabase
+
+    @BeforeTest
+    fun before() {
+        db = getRoomDatabase()
+    }
+
+    @AfterTest
+    fun after() {
+        db.close()
+    }
+
+    @Test
+    fun observeOneTable(): Unit = runBlocking {
+        val dao = db.dao()
+
+        val tableName = SampleEntity::class.simpleName!!
+        val observer = LatchObserver(tableName)
+
+        db.invalidationTracker.subscribe(observer)
+
+        dao.insertItem(1)
+
+        assertThat(observer.await()).isTrue()
+        assertThat(observer.invalidatedTables).containsExactly(tableName)
+
+        observer.reset()
+        db.invalidationTracker.unsubscribe(observer)
+
+        dao.insertItem(2)
+
+        assertThat(observer.await()).isFalse()
+        assertThat(observer.invalidatedTables).isNull()
+    }
+
+    private class LatchObserver(table: String) : InvalidationTracker.Observer(table) {
+
+        var invalidatedTables: Set<String>? = null
+            private set
+        private var latch = Mutex(locked = true)
+
+        override fun onInvalidated(tables: Set<String>) {
+            invalidatedTables = tables
+            latch.unlock()
+        }
+
+        suspend fun await(): Boolean {
+            try {
+                withTimeout(200) {
+                    latch.withLock { }
+                }
+                return true
+            } catch (ex: TimeoutCancellationException) {
+                return false
+            }
+        }
+
+        fun reset() {
+            invalidatedTables = null
+            latch = Mutex(locked = true)
+        }
+    }
+}
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseSimpleQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseSimpleQueryTest.kt
index 432595c..6a2c7e7 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseSimpleQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseSimpleQueryTest.kt
@@ -18,16 +18,31 @@
 
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
 import kotlin.test.Test
+import kotlinx.coroutines.flow.produceIn
 import kotlinx.coroutines.test.runTest
 
 abstract class BaseSimpleQueryTest {
 
+    private lateinit var db: SampleDatabase
+
     abstract fun getRoomDatabase(): SampleDatabase
 
+    @BeforeTest
+    fun before() {
+        db = getRoomDatabase()
+    }
+
+    @AfterTest
+    fun after() {
+        db.close()
+    }
+
     @Test
     fun preparedInsertAndDelete() = runTest {
-        val dao = getRoomDatabase().dao()
+        val dao = db.dao()
         assertThat(dao.insertItem(1)).isEqualTo(1)
         assertThat(dao.getSingleItem().pk).isEqualTo(1)
         assertThat(dao.deleteItem(1)).isEqualTo(1)
@@ -39,7 +54,7 @@
 
     @Test
     fun emptyResult() = runTest {
-        val db = getRoomDatabase()
+        val db = db
         assertThrows<IllegalStateException> {
             db.dao().getSingleItem()
         }.hasMessageThat().contains("The query result was empty")
@@ -47,7 +62,7 @@
 
     @Test
     fun queryList() = runTest {
-        val dao = getRoomDatabase().dao()
+        val dao = db.dao()
         dao.insertItem(1)
         dao.insertItem(2)
         dao.insertItem(3)
@@ -57,7 +72,7 @@
 
     @Test
     fun transactionDelegate() = runTest {
-        val dao = getRoomDatabase().dao()
+        val dao = db.dao()
         dao.insertItem(1)
         dao.insertItem(2)
         dao.insertItem(3)
@@ -79,7 +94,34 @@
     }
 
     @Test
-    fun simpleInsertAndDelete() = runTest {
+    fun queryFlow() = runTest {
+        val dao = getRoomDatabase().dao()
+        dao.insertItem(1)
+
+        val channel = dao.getItemListFlow().produceIn(this)
+
+        assertThat(channel.receive()).containsExactly(
+            SampleEntity(1)
+        )
+
+        dao.insertItem(2)
+        assertThat(channel.receive()).containsExactly(
+            SampleEntity(1),
+            SampleEntity(2),
+        )
+
+        dao.insertItem(3)
+        assertThat(channel.receive()).containsExactly(
+            SampleEntity(1),
+            SampleEntity(2),
+            SampleEntity(3),
+        )
+
+        channel.cancel()
+    }
+
+    @Test
+    fun insertAndDelete() = runTest {
         val sampleEntity = SampleEntity(1, 1)
         val dao = getRoomDatabase().dao()
 
@@ -93,7 +135,7 @@
     }
 
     @Test
-    fun simpleInsertAndUpdateAndDelete() = runTest {
+    fun insertAndUpdateAndDelete() = runTest {
         val sampleEntity1 = SampleEntity(1, 1)
         val sampleEntity2 = SampleEntity(1, 2)
         val dao = getRoomDatabase().dao()
@@ -111,7 +153,7 @@
     }
 
     @Test
-    fun simpleInsertAndUpsertAndDelete() = runTest {
+    fun insertAndUpsertAndDelete() = runTest {
         val sampleEntity1 = SampleEntity(1, 1)
         val sampleEntity2 = SampleEntity(1, 2)
         val dao = getRoomDatabase().dao()
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
index 01bceef..1deccba 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
@@ -30,13 +30,14 @@
 import androidx.room.Transaction
 import androidx.room.Update
 import androidx.room.Upsert
+import kotlinx.coroutines.flow.Flow
 
 @Entity
 data class SampleEntity(
     @PrimaryKey
     val pk: Long,
     @ColumnInfo(defaultValue = "0")
-    val data: Long
+    val data: Long = 0
 )
 
 @Entity
@@ -78,6 +79,9 @@
     @Query("SELECT * FROM SampleEntity")
     suspend fun getItemList(): List<SampleEntity>
 
+    @Query("SELECT * FROM SampleEntity")
+    fun getItemListFlow(): Flow<List<SampleEntity>>
+
     @Transaction
     suspend fun deleteList(pks: List<Long>, withError: Boolean = false) {
         require(!withError)
diff --git a/room/integration-tests/multiplatformtestapp/src/iosTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt b/room/integration-tests/multiplatformtestapp/src/iosTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt
new file mode 100644
index 0000000..1cc894a
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/iosTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.multiplatformtestapp.test
+
+import platform.Foundation.NSBundle
+
+/**
+ * Gets the schema directory path for tests with [androidx.room.testing.MigrationTestHelper].
+ *
+ * For iOS, it will be the main resource directory in the bundle.
+ */
+internal actual fun getSchemaDirectoryPath(): String {
+    return checkNotNull(NSBundle.mainBundle().resourcePath) + "/schemas-ksp"
+}
diff --git a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
index 1bf1332..19aca6e 100644
--- a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
@@ -17,15 +17,25 @@
 package androidx.room.integration.multiplatformtestapp.test
 
 import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
 import androidx.sqlite.SQLiteDriver
 import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import kotlin.io.path.Path
 import kotlin.io.path.createTempFile
+import org.junit.Rule
 
 class AutoMigrationTest : BaseAutoMigrationTest() {
-    private val tempFile = createTempFile(
-        "test.db"
-    ).also { it.toFile().deleteOnExit() }
-    override val driver: SQLiteDriver = BundledSQLiteDriver(tempFile.toString())
+    private val tempFile = createTempFile("test.db").also { it.toFile().deleteOnExit() }
+    private val driver: SQLiteDriver = BundledSQLiteDriver(tempFile.toString())
+
+    @get:Rule
+    val migrationTestHelper = MigrationTestHelper(
+        schemaDirectoryPath = Path("schemas-ksp"),
+        driver = driver,
+        databaseClass = AutoMigrationDatabase::class
+    )
+
+    override fun getTestHelper() = migrationTestHelper
 
     override fun getRoomDatabase(): AutoMigrationDatabase {
         return Room.databaseBuilder<AutoMigrationDatabase>(tempFile.toString())
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
similarity index 60%
copy from room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
copy to room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
index cc75983..8f52839 100644
--- a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
@@ -14,6 +14,16 @@
  * limitations under the License.
  */
 
-package androidx.room.migration.bundle
-// empty file to trigger klib creation
-// see: https://youtrack.jetbrains.com/issue/KT-52344
+package androidx.room.integration.multiplatformtestapp.test
+
+import androidx.room.Room
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+
+class InvalidationTest : BaseInvalidationTest() {
+
+    override fun getRoomDatabase(): SampleDatabase {
+        return Room.inMemoryDatabaseBuilder<SampleDatabase>()
+            .setDriver(BundledSQLiteDriver(":memory:"))
+            .build()
+    }
+}
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
index 5200b40..c35594c 100644
--- a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
@@ -17,6 +17,7 @@
 package androidx.room.integration.multiplatformtestapp.test
 
 import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
 import androidx.sqlite.SQLiteDriver
 import androidx.sqlite.driver.bundled.BundledSQLiteDriver
 import kotlin.random.Random
@@ -26,7 +27,16 @@
 
 class AutoMigrationTest : BaseAutoMigrationTest() {
     private val filename = "/tmp/test-${Random.nextInt()}.db"
-    override val driver: SQLiteDriver = BundledSQLiteDriver(filename)
+    private val driver: SQLiteDriver = BundledSQLiteDriver(filename)
+
+    private val migrationTestHelper = MigrationTestHelper(
+        schemaDirectoryPath = getSchemaDirectoryPath(),
+        driver = driver,
+        databaseClass = AutoMigrationDatabase::class,
+        databaseFactory = { AutoMigrationDatabase::class.instantiateImpl() }
+    )
+
+    override fun getTestHelper() = migrationTestHelper
 
     override fun getRoomDatabase(): AutoMigrationDatabase {
         return Room.databaseBuilder(filename) { AutoMigrationDatabase::class.instantiateImpl() }
@@ -40,6 +50,7 @@
 
     @AfterTest
     fun after() {
+        migrationTestHelper.finished()
         deleteDatabaseFile()
     }
 
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
new file mode 100644
index 0000000..8db97f5
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.integration.multiplatformtestapp.test
+
+import androidx.room.Room
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+
+class InvalidationTest : BaseInvalidationTest() {
+
+    override fun getRoomDatabase(): SampleDatabase {
+        return Room.inMemoryDatabaseBuilder { SampleDatabase::class.instantiateImpl() }
+            .setDriver(BundledSQLiteDriver(":memory:"))
+            .build()
+    }
+}
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt
similarity index 73%
copy from room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
copy to room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt
index cc75983..ba54f49 100644
--- a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.room.migration.bundle
-// empty file to trigger klib creation
-// see: https://youtrack.jetbrains.com/issue/KT-52344
+package androidx.room.integration.multiplatformtestapp.test
+
+/**
+ * Gets the schema directory path for tests with [androidx.room.testing.MigrationTestHelper]
+ */
+internal expect fun getSchemaDirectoryPath(): String
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt b/room/integration-tests/multiplatformtestapp/src/nonIosNativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt
similarity index 61%
copy from room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
copy to room/integration-tests/multiplatformtestapp/src/nonIosNativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt
index cc75983..9afd69d 100644
--- a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/Placeholder.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nonIosNativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SchemaDirectory.kt
@@ -14,6 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.room.migration.bundle
-// empty file to trigger klib creation
-// see: https://youtrack.jetbrains.com/issue/KT-52344
+package androidx.room.integration.multiplatformtestapp.test
+
+/**
+ * Gets the schema directory path for tests with [androidx.room.testing.MigrationTestHelper].
+ *
+ * For native (not iOS), it will be the directory in the project.
+ */
+// TODO(b/329526300): Investigate native resources for placing schemas.
+internal actual fun getSchemaDirectoryPath(): String = "schemas-ksp"
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index a79ba9c..dc31269 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -82,6 +82,7 @@
     coreLibraryDesugaring(libs.desugarJdkLibs)
     implementation(project(":room:room-common"))
     implementation(project(":room:room-runtime"))
+    implementation(project(":room:room-migration"))
     implementation("androidx.arch.core:core-runtime:2.2.0")
     implementation("androidx.lifecycle:lifecycle-livedata:2.6.1")
     implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java
deleted file mode 100644
index 0f2698df..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room;
-
-/**
- * Trojan class to be able to assert internal state.
- */
-public class InvalidationTrackerTrojan {
-    @SuppressWarnings("KotlinInternalInJava") // For testing
-    public static int countObservers(InvalidationTracker tracker) {
-        return tracker.getObserverMap$room_runtime_debug().size();
-    }
-
-    private InvalidationTrackerTrojan() {
-    }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
index 0996752..fa12d0a 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
@@ -22,13 +22,13 @@
 import android.database.SQLException;
 
 import androidx.annotation.NonNull;
-import androidx.room.DatabaseConfiguration;
 import androidx.room.migration.Migration;
 import androidx.room.testing.MigrationTestHelper;
 import androidx.room.util.TableInfo;
 import androidx.sqlite.db.SupportSQLiteDatabase;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Rule;
@@ -42,6 +42,7 @@
  */
 @RunWith(AndroidJUnit4.class)
 @LargeTest
+@SdkSuppress(minSdkVersion = 22) // b/329236938
 public class AutoMigrationTest {
     private static final String TEST_DB = "auto-migration-test";
     @Rule
@@ -145,10 +146,6 @@
                 true,
                 MIGRATION_1_0
         );
-        DatabaseConfiguration config = helper.databaseConfiguration;
-        assertThat(config).isNotNull();
-        assertThat(config.migrationContainer.findMigrationPath(1, 2)).isNotNull();
-        assertThat(config.migrationContainer.findMigrationPath(1, 2)).isNotEmpty();
     }
 
     private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
index 0be35ef..b8b3378 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
@@ -1006,7 +1006,7 @@
                 Context testContext = InstrumentationRegistry.getInstrumentation().getContext();
                 InputStream input = testContext.getAssets().open(MigrationDb.class.getCanonicalName()
                         + "/" + MigrationDb.LATEST_VERSION + ".json");
-                SchemaBundle schemaBundle = SchemaBundle.deserialize(input);
+                SchemaBundle schemaBundle = SchemaBundle.Companion.deserialize(input);
                 for (String query : schemaBundle.getDatabase().buildCreateQueries()) {
                     db.execSQL(query);
                 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
index d4a92d7..0419088 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
@@ -51,6 +51,7 @@
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 
+// TODO: Consolidate with AutoClosingDatabaseTest that has access to internal APIs.
 public class AutoClosingRoomOpenHelperTest {
     @Rule
     public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
@@ -315,41 +316,6 @@
         db.close();
     }
 
-    @Test
-    @MediumTest
-    public void invalidationObserver_notifiedByTableName() throws TimeoutException,
-            InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-
-        context.deleteDatabase("testDb2");
-        TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
-                // create contention for callback
-                .setAutoCloseTimeout(0, TimeUnit.MILLISECONDS)
-                .addCallback(mCallback).build();
-
-        AtomicInteger invalidationCount = new AtomicInteger(0);
-
-        UserTableObserver userTableObserver =
-                new UserTableObserver(invalidationCount::getAndIncrement);
-
-        db.getInvalidationTracker().addObserver(userTableObserver);
-
-
-        db.getUserDao().insert(TestUtil.createUser(1));
-
-        drain();
-        assertEquals(1, invalidationCount.get());
-
-        Thread.sleep(100); // Let db auto close
-
-        db.getInvalidationTracker().notifyObserversByTableNames("user");
-
-        drain();
-        assertEquals(2, invalidationCount.get());
-
-        db.close();
-    }
-
     private void drain() throws TimeoutException, InterruptedException {
         mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
     }
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
index 87155d8..98c7dd5 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
@@ -30,7 +30,6 @@
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.Observer;
 import androidx.lifecycle.testing.TestLifecycleOwner;
-import androidx.room.InvalidationTrackerTrojan;
 import androidx.room.Room;
 import androidx.room.integration.testapp.FtsTestDatabase;
 import androidx.room.integration.testapp.MusicTestDatabase;
@@ -465,8 +464,6 @@
         TestUtil.forceGc();
         mUserDao.updateById(3, "Bar");
         TestUtil.forceGc();
-        assertThat(InvalidationTrackerTrojan.countObservers(mDatabase.getInvalidationTracker()),
-                is(0));
         assertThat(weakLiveData.get(), nullValue());
     }
 
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultiInstanceInvalidationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultiInstanceInvalidationTest.java
index aad4cc0..0e71304 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultiInstanceInvalidationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultiInstanceInvalidationTest.java
@@ -28,6 +28,7 @@
 import android.os.SystemClock;
 
 import androidx.annotation.NonNull;
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
 import androidx.collection.SimpleArrayMap;
 import androidx.core.util.Pair;
 import androidx.lifecycle.LiveData;
@@ -87,6 +88,9 @@
     @Rule
     public final ServiceTestRule serviceRule = new ServiceTestRule();
 
+    @Rule
+    public final CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
     private ISampleDatabaseService mService;
 
     private String mDatabaseName;
@@ -105,7 +109,7 @@
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws InterruptedException, TimeoutException {
         for (int i = 0, size = mObservers.size(); i < size; i++) {
             final LiveData<List<Customer>> liveData = mObservers.keyAt(i);
             final Observer<List<Customer>> observer = mObservers.valueAt(i);
@@ -118,6 +122,7 @@
         for (int i = 0, size = mDatabases.size(); i < size; i++) {
             mDatabases.get(i).close();
         }
+        mExecutorRule.drainTasks(2, TimeUnit.SECONDS);
     }
 
     // TODO(324609478): broken test
@@ -241,7 +246,8 @@
         final CountDownLatch invalidated1 = prepareTableObserver(db1);
         final CountDownLatch changed1 = prepareLiveDataObserver(db1).first;
 
-        db2.getInvalidationTracker().notifyObserversByTableNames("Customer");
+        db2.getCustomerDao().insert(CUSTOMER_1);
+        mExecutorRule.drainTasks(300, TimeUnit.MILLISECONDS);
 
         assertFalse(invalidated1.await(300, TimeUnit.MILLISECONDS));
         assertFalse(changed1.await(300, TimeUnit.MILLISECONDS));
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/QueryTransactionTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/QueryTransactionTest.java
index cb7238a..19e89b2 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/QueryTransactionTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/QueryTransactionTest.java
@@ -298,12 +298,12 @@
     }
 
     private static void incrementTransactionCount() {
-        // When incrementing the transaction count, ignore those coming from the refresh runnable
+        // When incrementing the transaction count, ignore those coming from the refresh
         // in the invalidation tracker.
         StackTraceElement[] stack = Thread.currentThread().getStackTrace();
         for (StackTraceElement element : stack) {
             String fileName = element.getFileName();
-            if (fileName != null && fileName.equals("InvalidationTracker.android.kt")) {
+            if (fileName != null && fileName.equals("InvalidationTracker.kt")) {
                 return;
             }
         }
@@ -316,7 +316,7 @@
 
     private void drain() {
         try {
-            countingTaskExecutorRule.drainTasks(30, TimeUnit.SECONDS);
+            countingTaskExecutorRule.drainTasks(3, TimeUnit.SECONDS);
         } catch (InterruptedException e) {
             throw new AssertionError("interrupted", e);
         } catch (TimeoutException e) {
@@ -603,21 +603,21 @@
         public void beginTransactionWithListener(
                 @NonNull SQLiteTransactionListener transactionListener) {
             mDelegate.beginTransactionWithListener(transactionListener);
-            sStartedTransactionCount.incrementAndGet();
+            incrementTransactionCount();
         }
 
         @Override
         public void beginTransactionWithListenerNonExclusive(
                 @NonNull SQLiteTransactionListener transactionListener) {
             mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
-            sStartedTransactionCount.incrementAndGet();
+            incrementTransactionCount();
         }
 
         @Override
         public void beginTransactionWithListenerReadOnly(
                 @NonNull SQLiteTransactionListener transactionListener) {
             mDelegate.beginTransactionWithListenerReadOnly(transactionListener);
-            sStartedTransactionCount.incrementAndGet();
+            incrementTransactionCount();
         }
 
         @Override
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
index 94cd311..0171412 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
@@ -193,6 +193,12 @@
          * respectively.
          */
         fun getArrayName(componentTypeName: XTypeName): XTypeName {
+            componentTypeName.java.let {
+                require(it !is JWildcardTypeName || it.lowerBounds.isEmpty()) {
+                    "Can't have contra-variant component types in Java arrays. Found '$it'."
+                }
+            }
+
             val (java, kotlin) = when (componentTypeName) {
                 PRIMITIVE_BOOLEAN ->
                     JArrayTypeName.of(JTypeName.BOOLEAN) to BOOLEAN_ARRAY
@@ -210,9 +216,16 @@
                     JArrayTypeName.of(JTypeName.FLOAT) to FLOAT_ARRAY
                 PRIMITIVE_DOUBLE ->
                     JArrayTypeName.of(JTypeName.DOUBLE) to DOUBLE_ARRAY
-                else ->
-                    JArrayTypeName.of(componentTypeName.java) to
-                        ARRAY.parameterizedBy(componentTypeName.kotlin)
+                else -> {
+                    componentTypeName.java.let {
+                        if (it is JWildcardTypeName) {
+                            JArrayTypeName.of(it.upperBounds.single())
+                        } else {
+                            JArrayTypeName.of(it)
+                        }
+                    } to
+                    ARRAY.parameterizedBy(componentTypeName.kotlin)
+                }
             }
             return XTypeName(
                 java = java,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
index 15a9043..a0549e5 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
@@ -20,6 +20,7 @@
 import androidx.room.compiler.processing.XExecutableParameterElement
 import androidx.room.compiler.processing.XMemberContainer
 import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.isArray
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_METHOD_PARAMETER
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
 import androidx.room.compiler.processing.util.sanitizeAsJavaParameterName
@@ -74,7 +75,7 @@
     private fun createAsMemberOf(container: XType?): KspType {
         check(container is KspType?)
         val resolvedType = parameter.type.resolve()
-        return env.wrap(
+        val type = env.wrap(
             originalAnnotations = parameter.type.annotations,
             ksType = parameter.typeAsMemberOf(
                 functionDeclaration = enclosingElement.declaration,
@@ -91,6 +92,13 @@
                 asMemberOf = container,
             )
         )
+        // In KSP2 the varargs have the component type instead of the array type. We make it always
+        // return the array type in XProcessing.
+        return if (isVarArgs() && !type.isArray()) {
+            env.getArrayType(type)
+        } else {
+            type
+        }
     }
 
     override fun kindName(): String {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
index e93dfd6..f42e1f5 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.codegen
 
 import androidx.kruth.assertThat
+import androidx.kruth.assertThrows
 import androidx.room.compiler.processing.XNullability
 import com.squareup.kotlinpoet.INT
 import com.squareup.kotlinpoet.SHORT
@@ -176,4 +177,25 @@
         assertThat(XTypeName.getProducerExtendsName(typeName).kotlin)
             .isEqualTo(XTypeName.UNAVAILABLE_KTYPE_NAME)
     }
+
+    @Test
+    fun arrays() {
+        XTypeName.getArrayName(Number::class.asClassName()).let {
+            assertThat(it.toString(CodeLanguage.JAVA))
+                .isEqualTo("java.lang.Number[]")
+            assertThat(it.toString(CodeLanguage.KOTLIN))
+                .isEqualTo("kotlin.Array<kotlin.Number>")
+        }
+        XTypeName.getArrayName(
+            XTypeName.getProducerExtendsName(Number::class.asClassName())).let {
+            assertThat(it.toString(CodeLanguage.JAVA))
+                .isEqualTo("java.lang.Number[]")
+            assertThat(it.toString(CodeLanguage.KOTLIN))
+                .isEqualTo("kotlin.Array<out kotlin.Number>")
+        }
+        assertThrows<IllegalArgumentException> {
+            XTypeName.getArrayName(XTypeName.getConsumerSuperName(Number::class.asClassName()))
+        }.hasMessageThat().isEqualTo("Can't have contra-variant component types in Java " +
+            "arrays. Found '? super java.lang.Number'.")
+    }
 }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index fab71288..d60da50 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -108,6 +108,7 @@
             package foo.bar;
             interface Baz {
                 void method(String... inputs);
+                void methodPrimitive(int... inputs);
             }
             """.trimIndent()
         )
@@ -115,7 +116,16 @@
             sources = listOf(subject)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
-            assertThat(element.getMethodByJvmName("method").isVarArgs()).isTrue()
+            element.getMethodByJvmName("method").let { method ->
+                assertThat(method.isVarArgs()).isTrue()
+                assertThat(method.parameters.single().type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(String::class.asClassName()))
+            }
+            element.getMethodByJvmName("methodPrimitive").let { method ->
+                assertThat(method.isVarArgs()).isTrue()
+                assertThat(method.parameters.single().type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(XTypeName.PRIMITIVE_INT))
+            }
         }
     }
 
@@ -128,6 +138,7 @@
                 fun method(vararg inputs: String)
                 suspend fun suspendMethod(vararg inputs: String)
                 fun method2(vararg inputs: String, arg: Int)
+                fun methodPrimitive(vararg inputs: Int)
                 fun String.extFun(vararg inputs: String)
             }
             """.trimIndent()
@@ -141,6 +152,10 @@
                 assertThat(method.isVarArgs()).isTrue()
                 assertThat(method.parameters).hasSize(1)
                 assertThat(method.parameters.single().isVarArgs()).isTrue()
+                assertThat(method.parameters.single().type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(
+                        XTypeName.getProducerExtendsName(String::class.asClassName()))
+                )
             }
 
             element.getMethodByJvmName("suspendMethod").let { suspendMethod ->
@@ -149,6 +164,12 @@
                 assertThat(
                     suspendMethod.parameters.first { it.name == "inputs" }.isVarArgs()
                 ).isTrue()
+                assertThat(
+                    suspendMethod.parameters.first { it.name == "inputs" }.type.asTypeName()
+                ).isEqualTo(
+                    XTypeName.getArrayName(
+                        XTypeName.getProducerExtendsName(String::class.asClassName()))
+                )
             }
 
             element.getMethodByJvmName("extFun").let { extFun ->
@@ -157,6 +178,10 @@
                 // kapt messed with parameter names, sometimes the synthetic parameter can use the
                 // second parameter's name.
                 assertThat(extFun.parameters.get(1).isVarArgs()).isTrue()
+                assertThat(extFun.parameters.get(1).type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(
+                        XTypeName.getProducerExtendsName(String::class.asClassName()))
+                )
             }
 
             element.getMethodByJvmName("method2").let { method2 ->
@@ -164,6 +189,19 @@
                 assertThat(method2.parameters).hasSize(2)
                 assertThat(method2.parameters.first { it.name == "inputs" }.isVarArgs())
                     .isTrue()
+                assertThat(method2.parameters.first { it.name == "inputs" }.type.asTypeName())
+                    .isEqualTo(
+                        XTypeName.getArrayName(
+                            XTypeName.getProducerExtendsName(String::class.asClassName()))
+                    )
+            }
+            element.getMethodByJvmName("methodPrimitive").let { method ->
+                assertThat(method.isVarArgs()).isTrue()
+                assertThat(method.parameters).hasSize(1)
+                assertThat(method.parameters.single().isVarArgs()).isTrue()
+                assertThat(method.parameters.single().type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(XTypeName.PRIMITIVE_INT)
+                )
             }
         }
     }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 9578966..7b40165 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -87,6 +87,7 @@
         XClassName.get(ROOM_PACKAGE, "RoomOpenDelegate", "ValidationResult")
     val STATEMENT_UTIL = XClassName.get("$ROOM_PACKAGE.util", "SQLiteStatementUtil")
     val CONNECTION_UTIL = XClassName.get("$ROOM_PACKAGE.util", "SQLiteConnectionUtil")
+    val FLOW_UTIL = XClassName.get("$ROOM_PACKAGE.coroutines", "FlowUtil")
 }
 
 object RoomAnnotationTypeNames {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
index 641a5c0..135ebb4 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -28,7 +28,6 @@
 import androidx.room.migration.bundle.SchemaBundle
 import androidx.room.processor.ProcessorErrors.AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF
 import androidx.room.processor.ProcessorErrors.AUTO_MIGRATION_SCHEMA_IN_FOLDER_NULL
-import androidx.room.processor.ProcessorErrors.autoMigrationSchemaIsEmpty
 import androidx.room.processor.ProcessorErrors.invalidAutoMigrationSchema
 import androidx.room.util.SchemaFileResolver
 import androidx.room.verifier.DatabaseVerificationErrors
@@ -42,6 +41,7 @@
 import androidx.room.vo.Warning
 import androidx.room.vo.columnNames
 import androidx.room.vo.findFieldByColumnName
+import java.io.FileNotFoundException
 import java.io.IOException
 import java.nio.file.Path
 import java.util.Locale
@@ -230,24 +230,25 @@
             schemaStream.use {
                 SchemaBundle.deserialize(schemaStream)
             }
+        } catch (ex: FileNotFoundException) {
+            context.logger.e(
+                element,
+                ProcessorErrors.autoMigrationSchemasNotFound(
+                    version,
+                    schemaFolderPath.toString()
+                ),
+            )
+            null
         } catch (th: Throwable) {
-            if (th is SchemaBundle.EmptySchemaException) {
-                context.logger.e(
-                    element,
-                    autoMigrationSchemaIsEmpty(
-                        version,
-                        schemaFolderPath.toString()
-                    ),
+            // For debugging support include exception message in an error too.
+            context.logger.e("Unable to read schema file: ${th.message ?: ""}")
+            context.logger.e(
+                element,
+                invalidAutoMigrationSchema(
+                    version,
+                    schemaFolderPath.toString()
                 )
-            } else {
-                context.logger.e(
-                    element,
-                    invalidAutoMigrationSchema(
-                        version,
-                        schemaFolderPath.toString()
-                    )
-                )
-            }
+            )
             null
         }
         return bundle?.database
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index a293e29..99e1ccf 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -926,11 +926,6 @@
             "out folder: $schemaOutFolderPath. Cannot generate auto migrations."
     }
 
-    fun autoMigrationSchemaIsEmpty(schemaVersion: Int, schemaOutFolderPath: String): String {
-        return "Found empty schema file '$schemaVersion.json' required for migration was not " +
-            "found at the schema out folder: $schemaOutFolderPath. Cannot generate auto migrations."
-    }
-
     fun invalidAutoMigrationSchema(schemaVersion: Int, schemaOutFolderPath: String): String {
         return "Found invalid schema file '$schemaVersion.json' at the schema out " +
             "folder: $schemaOutFolderPath.\nIf you've modified the file, you might've broken the " +
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineFlowResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineFlowResultBinder.kt
index fc5e066..2a7ff10 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineFlowResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineFlowResultBinder.kt
@@ -16,17 +16,24 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.XMemberName.Companion.packageMember
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.ArrayLiteral
 import androidx.room.ext.CallableTypeSpecBuilder
 import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.Function1TypeSpec
 import androidx.room.ext.RoomCoroutinesTypeNames.COROUTINES_ROOM
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SQLiteDriverTypeNames
 import androidx.room.solver.CodeGenScope
 
 /**
- * Binds the result of a of a Kotlin Coroutine Flow<T>
+ * Binds the result of a Kotlin Coroutine Flow<T>
  */
 class CoroutineFlowResultBinder(
     val typeArg: XType,
@@ -76,4 +83,124 @@
             )
         }
     }
+
+    override fun isMigratedToDriver() = adapter?.isMigratedToDriver() == true
+
+    override fun convertAndReturn(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: CodeGenScope.(String) -> Unit,
+        returnTypeName: XTypeName,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        val arrayOfTableNamesLiteral = ArrayLiteral(
+            scope.language,
+            CommonTypeNames.STRING,
+            *tableNames.toTypedArray()
+        )
+        when (scope.language) {
+            CodeLanguage.JAVA -> convertAndReturnJava(
+                sqlQueryVar,
+                dbProperty,
+                bindStatement,
+                inTransaction,
+                arrayOfTableNamesLiteral,
+                scope
+            )
+
+            CodeLanguage.KOTLIN -> convertAndReturnKotlin(
+                sqlQueryVar,
+                dbProperty,
+                bindStatement,
+                inTransaction,
+                arrayOfTableNamesLiteral,
+                scope
+            )
+        }
+    }
+
+    private fun convertAndReturnJava(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: CodeGenScope.(String) -> Unit,
+        inTransaction: Boolean,
+        arrayOfTableNamesLiteral: XCodeBlock,
+        scope: CodeGenScope
+    ) {
+        val connectionVar = scope.getTmpVar("_connection")
+        val statementVar = scope.getTmpVar("_stmt")
+        scope.builder.addStatement(
+            "return %M(%N, %L, %L, %L)",
+            RoomTypeNames.FLOW_UTIL.packageMember("createFlow"),
+            dbProperty,
+            inTransaction,
+            arrayOfTableNamesLiteral,
+            // TODO(b/322387497): Generate lambda syntax if possible
+            Function1TypeSpec(
+                language = scope.language,
+                parameterTypeName = SQLiteDriverTypeNames.CONNECTION,
+                parameterName = connectionVar,
+                returnTypeName = typeArg.asTypeName()
+            ) {
+                val functionScope = scope.fork()
+                val outVar = functionScope.getTmpVar("_result")
+                val functionCode = functionScope.builder.apply {
+                    addLocalVal(
+                        statementVar,
+                        SQLiteDriverTypeNames.STATEMENT,
+                        "%L.prepare(%L)",
+                        connectionVar,
+                        sqlQueryVar
+                    )
+                    beginControlFlow("try")
+                    bindStatement(functionScope, statementVar)
+                    adapter?.convert(outVar, statementVar, functionScope)
+                    addStatement("return %L", outVar)
+                    nextControlFlow("finally")
+                    addStatement("%L.close()", statementVar)
+                    endControlFlow()
+                }.build()
+                this.addCode(functionCode)
+            }
+        )
+    }
+
+    private fun convertAndReturnKotlin(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: CodeGenScope.(String) -> Unit,
+        inTransaction: Boolean,
+        arrayOfTableNamesLiteral: XCodeBlock,
+        scope: CodeGenScope
+    ) {
+        val connectionVar = scope.getTmpVar("_connection")
+        val statementVar = scope.getTmpVar("_stmt")
+        scope.builder.apply {
+            beginControlFlow(
+                "return %M(%N, %L, %L) { %L ->",
+                RoomTypeNames.FLOW_UTIL.packageMember("createFlow"),
+                dbProperty,
+                inTransaction,
+                arrayOfTableNamesLiteral,
+                connectionVar
+            )
+            addLocalVal(
+                statementVar,
+                SQLiteDriverTypeNames.STATEMENT,
+                "%L.prepare(%L)",
+                connectionVar,
+                sqlQueryVar
+            )
+            beginControlFlow("try")
+            bindStatement(scope, statementVar)
+            val outVar = scope.getTmpVar("_result")
+            adapter?.convert(outVar, statementVar, scope)
+            addStatement("%L", outVar)
+            nextControlFlow("finally")
+            addStatement("%L.close()", statementVar)
+            endControlFlow()
+            endControlFlow()
+        }
+    }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt b/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
index 9d59d4d..80944a1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.util
 
+import androidx.room.migration.bundle.BaseEntityBundle
 import androidx.room.migration.bundle.DatabaseBundle
 import androidx.room.migration.bundle.DatabaseViewBundle
 import androidx.room.migration.bundle.EntityBundle
@@ -80,7 +81,7 @@
     private val potentiallyDeletedTables = mutableSetOf<String>()
     // Maps FTS tables in the to version to the name of their content tables in the from version
     // for easy lookup.
-    private val contentTableToFtsEntities = mutableMapOf<String, MutableList<EntityBundle>>()
+    private val contentTableToFtsEntities = mutableMapOf<String, MutableList<BaseEntityBundle>>()
 
     private val addedTables = mutableSetOf<AutoMigration.AddedTable>()
     // Any table that has been renamed, but also does not contain any complex changes.
@@ -196,8 +197,8 @@
      * null object will be returned.
      */
     private fun detectTableLevelChanges(
-        fromTable: EntityBundle
-    ): EntityBundle? {
+        fromTable: BaseEntityBundle
+    ): BaseEntityBundle? {
         // Check if the table was renamed. If so, check for other complex changes that could
         // be found on the table level. Save the end result to the complex changed tables map.
         val renamedTable = isTableRenamed(fromTable.tableName)
@@ -283,8 +284,8 @@
      * value if the column was deleted.
      */
     private fun detectColumnLevelChanges(
-        fromTable: EntityBundle,
-        toTable: EntityBundle,
+        fromTable: BaseEntityBundle,
+        toTable: BaseEntityBundle,
         fromColumn: FieldBundle,
     ): String? {
         // Check if this column was renamed. If so, no need to check further, we can mark this
@@ -368,39 +369,37 @@
      * @return A ComplexChangedTable object, null if complex schema change has not been found
      */
     private fun tableContainsComplexChanges(
-        fromTable: EntityBundle,
-        toTable: EntityBundle
+        fromTable: BaseEntityBundle,
+        toTable: BaseEntityBundle
     ): Boolean {
-        // If we have an FTS table, check if options have changed
-        if (fromTable is FtsEntityBundle &&
-            toTable is FtsEntityBundle &&
-            !fromTable.ftsOptions.isSchemaEqual(toTable.ftsOptions)
-        ) {
-            return true
-        }
-        // Check if the to table or the from table is an FTS table while the other is not.
-        if (fromTable is FtsEntityBundle && toTable !is FtsEntityBundle ||
-            toTable is FtsEntityBundle && fromTable !is FtsEntityBundle
-        ) {
-            return true
-        }
-
-        if (!isForeignKeyBundlesListEqual(fromTable.foreignKeys, toTable.foreignKeys)) {
-            return true
-        }
-        if (!isIndexBundlesListEqual(fromTable.indices, toTable.indices)) {
-            return true
-        }
-
         if (!fromTable.primaryKey.isSchemaEqual(toTable.primaryKey)) {
             return true
         }
-        // Check if any foreign keys are referencing a renamed table.
-        return fromTable.foreignKeys.any { foreignKey ->
-            renameTableEntries.any {
-                it.originalTableName == foreignKey.table
+
+        // If both are FTS tables, only check if options have changed
+        if (fromTable is FtsEntityBundle && toTable is FtsEntityBundle) {
+            return !fromTable.ftsOptions.isSchemaEqual(toTable.ftsOptions)
+        }
+
+        // If both are normal tables, check foreign keys and indices
+        if (fromTable is EntityBundle && toTable is EntityBundle) {
+            if (!isForeignKeyBundlesListEqual(fromTable.foreignKeys, toTable.foreignKeys)) {
+                return true
+            }
+            if (!isIndexBundlesListEqual(fromTable.indices, toTable.indices)) {
+                return true
+            }
+            // Check if any foreign keys are referencing a renamed table.
+            return fromTable.foreignKeys.any { foreignKey ->
+                renameTableEntries.any {
+                    it.originalTableName == foreignKey.table
+                }
             }
         }
+
+        // If we reach this check then from and to tables are not of the same type, a change of
+        // table type is complex
+        return true
     }
 
     /**
@@ -525,7 +524,7 @@
      * database that have been already processed
      */
     private fun processAddedTableAndColumns(
-        toTable: EntityBundle,
+        toTable: BaseEntityBundle,
         processedTablesAndColumnsInNewVersion: MutableMap<String, List<String>>
     ) {
         // Old table bundle will be found even if table is renamed.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
index 35248a6..7900a4e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
@@ -18,7 +18,7 @@
 
 import androidx.room.compiler.codegen.XClassName
 import androidx.room.compiler.processing.XTypeElement
-import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.BaseEntityBundle
 import androidx.room.migration.bundle.FieldBundle
 import androidx.room.util.SchemaDiffResult
 
@@ -65,7 +65,7 @@
     /**
      * Stores the table that was added to a database in a newer version.
      */
-    data class AddedTable(val entityBundle: EntityBundle)
+    data class AddedTable(val entityBundle: BaseEntityBundle)
 
     /**
      * Stores the table that contains a change in the primary key, foreign key(s) or index(es)
@@ -82,8 +82,8 @@
     data class ComplexChangedTable(
         val tableName: String,
         val tableNameWithNewPrefix: String,
-        val oldVersionEntityBundle: EntityBundle,
-        val newVersionEntityBundle: EntityBundle,
+        val oldVersionEntityBundle: BaseEntityBundle,
+        val newVersionEntityBundle: BaseEntityBundle,
         val renamedColumnsMap: MutableMap<String, String>
     )
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
index 04dec4a..78d0d1c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
@@ -21,6 +21,7 @@
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.SCHEMA_LATEST_FORMAT_VERSION
 import androidx.room.migration.bundle.SchemaBundle
 import androidx.room.util.SchemaFileResolver
 import java.io.IOException
@@ -107,7 +108,7 @@
     // Writes schema file to output path, using the input path to check if the schema has changed
     // otherwise it is not written.
     fun exportSchema(inputPath: Path, outputPath: Path) {
-        val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
+        val schemaBundle = SchemaBundle(SCHEMA_LATEST_FORMAT_VERSION, bundle)
         val inputStream = try {
             SchemaFileResolver.RESOLVER.readPath(inputPath)
         } catch (e: IOException) {
@@ -136,7 +137,7 @@
     // existing schema equality, otherwise use the version of `exportSchema` that takes input and
     // output paths.
     fun exportSchemaOnly(outputStream: OutputStream) {
-        val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
+        val schemaBundle = SchemaBundle(SCHEMA_LATEST_FORMAT_VERSION, bundle)
         SchemaBundle.serialize(schemaBundle, outputStream)
     }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Entity.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Entity.kt
index 6f4b932..4fbc12f 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Entity.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Entity.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.migration.bundle.BaseEntityBundle
 import androidx.room.migration.bundle.EntityBundle
 import androidx.room.migration.bundle.TABLE_NAME_PLACEHOLDER
 
@@ -87,7 +88,7 @@
         }
     }
 
-    open fun toBundle(): EntityBundle = EntityBundle(
+    open fun toBundle(): BaseEntityBundle = EntityBundle(
         tableName,
         createTableQuery(TABLE_NAME_PLACEHOLDER),
         fields.map { it.toBundle() },
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/FtsEntity.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/FtsEntity.kt
index 5a8a9a9..3f8255f 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/FtsEntity.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/FtsEntity.kt
@@ -133,6 +133,8 @@
         createTableQuery(TABLE_NAME_PLACEHOLDER),
         nonHiddenFields.map { it.toBundle() },
         primaryKey.toBundle(),
+        emptyList(),
+        emptyList(),
         ftsVersion.name,
         ftsOptions.toBundle(),
         contentSyncTriggerCreateQueries
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
index 0e4a495..ab93879 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
@@ -29,6 +29,7 @@
 import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.SQLiteDriverMemberNames
 import androidx.room.ext.SQLiteDriverTypeNames.CONNECTION
+import androidx.room.migration.bundle.BaseEntityBundle
 import androidx.room.migration.bundle.EntityBundle
 import androidx.room.migration.bundle.FtsEntityBundle
 import androidx.room.vo.AutoMigration
@@ -201,12 +202,14 @@
                     tableNameWithNewPrefix,
                     migrateBuilder
                 )
-                addStatementsToRecreateIndexes(newEntityBundle, migrateBuilder)
-                if (newEntityBundle.foreignKeys.isNotEmpty()) {
-                    addStatementsToCheckForeignKeyConstraint(
-                        newEntityBundle.tableName,
-                        migrateBuilder
-                    )
+                if (newEntityBundle is EntityBundle) {
+                    addStatementsToRecreateIndexes(newEntityBundle, migrateBuilder)
+                    if (newEntityBundle.foreignKeys.isNotEmpty()) {
+                        addStatementsToCheckForeignKeyConstraint(
+                            newEntityBundle.tableName,
+                            migrateBuilder
+                        )
+                    }
                 }
             }
         }
@@ -214,8 +217,8 @@
 
     private fun addStatementsToMigrateFtsTable(
         migrateBuilder: XFunSpec.Builder,
-        oldTable: EntityBundle,
-        newTable: EntityBundle,
+        oldTable: BaseEntityBundle,
+        newTable: BaseEntityBundle,
         renamedColumnsMap: MutableMap<String, String>
     ) {
         addDatabaseExecuteSqlStatement(migrateBuilder, "DROP TABLE `${oldTable.tableName}`")
@@ -274,7 +277,7 @@
      * @param migrateBuilder Builder for the migrate() function to be generated
      */
     private fun addStatementsToCreateNewTable(
-        newTable: EntityBundle,
+        newTable: BaseEntityBundle,
         migrateBuilder: XFunSpec.Builder
     ) {
         addDatabaseExecuteSqlStatement(
@@ -296,8 +299,8 @@
     private fun addStatementsToContentTransfer(
         oldTableName: String,
         tableNameWithNewPrefix: String,
-        oldEntityBundle: EntityBundle,
-        newEntityBundle: EntityBundle,
+        oldEntityBundle: BaseEntityBundle,
+        newEntityBundle: BaseEntityBundle,
         renamedColumnsMap: MutableMap<String, String>,
         migrateBuilder: XFunSpec.Builder
     ) {
@@ -463,7 +466,9 @@
                 migrateBuilder,
                 addedTable.entityBundle.createTable()
             )
-            addStatementsToRecreateIndexes(addedTable.entityBundle, migrateBuilder)
+            if (addedTable.entityBundle is EntityBundle) {
+                addStatementsToRecreateIndexes(addedTable.entityBundle, migrateBuilder)
+            }
         }
     }
 
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
index d8fde6c..f74dcbb 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
@@ -1370,9 +1370,10 @@
             USER, AUTOMIGRATION,
         ) { _, invocation ->
             invocation.assertCompilationResult {
-                hasErrorCount(1)
+                hasErrorCount(2)
+                hasErrorContaining("Unable to read schema file")
                 hasErrorContaining(
-                    ProcessorErrors.autoMigrationSchemaIsEmpty(
+                    ProcessorErrors.invalidAutoMigrationSchema(
                         1,
                         schemaFolder.root.absolutePath + File.separator + "foo.bar.MyDb"
                     )
@@ -1401,7 +1402,8 @@
             USER, AUTOMIGRATION,
         ) { _, invocation ->
             invocation.assertCompilationResult {
-                hasErrorCount(1)
+                hasErrorCount(2)
+                hasErrorContaining("Unable to read schema file")
                 hasErrorContaining(
                     invalidAutoMigrationSchema(
                         1,
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
index 0abb567..64bc0a4 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
@@ -1,16 +1,11 @@
-import android.database.Cursor
-import androidx.room.CoroutinesRoom
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.coroutines.createFlow
 import androidx.room.util.appendPlaceholders
 import androidx.room.util.getColumnIndexOrThrow
 import androidx.room.util.getLastInsertedRowId
 import androidx.room.util.getTotalChangedRows
 import androidx.room.util.performSuspending
-import androidx.room.util.query
 import androidx.sqlite.SQLiteStatement
-import java.util.concurrent.Callable
 import javax.`annotation`.processing.Generated
 import kotlin.Int
 import kotlin.Long
@@ -40,43 +35,35 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String? in arg) {
-      if (_item == null) {
-        _statement.bindNull(_argIndex)
-      } else {
-        _statement.bindString(_argIndex, _item)
-      }
-      _argIndex++
-    }
-    return CoroutinesRoom.createFlow(__db, false, arrayOf("MyEntity"), object : Callable<MyEntity> {
-      public override fun call(): MyEntity {
-        val _cursor: Cursor = query(__db, _statement, false, null)
-        try {
-          val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-          val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
-          val _result: MyEntity
-          if (_cursor.moveToFirst()) {
-            val _tmpPk: Int
-            _tmpPk = _cursor.getInt(_cursorIndexOfPk)
-            val _tmpOther: String
-            _tmpOther = _cursor.getString(_cursorIndexOfOther)
-            _result = MyEntity(_tmpPk,_tmpOther)
+    return createFlow(__db, false, arrayOf("MyEntity")) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        var _argIndex: Int = 1
+        for (_item: String? in arg) {
+          if (_item == null) {
+            _stmt.bindNull(_argIndex)
           } else {
-            error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+            _stmt.bindText(_argIndex, _item)
           }
-          return _result
-        } finally {
-          _cursor.close()
+          _argIndex++
         }
+        val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+        val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
+        val _result: MyEntity
+        if (_stmt.step()) {
+          val _tmpPk: Int
+          _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
+          val _tmpOther: String
+          _tmpOther = _stmt.getText(_cursorIndexOfOther)
+          _result = MyEntity(_tmpPk,_tmpOther)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-
-      protected fun finalize() {
-        _statement.release()
-      }
-    })
+    }
   }
 
   public override fun getFlowNullable(vararg arg: String?): Flow<MyEntity?> {
@@ -86,44 +73,35 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String? in arg) {
-      if (_item == null) {
-        _statement.bindNull(_argIndex)
-      } else {
-        _statement.bindString(_argIndex, _item)
-      }
-      _argIndex++
-    }
-    return CoroutinesRoom.createFlow(__db, false, arrayOf("MyEntity"), object : Callable<MyEntity?>
-        {
-      public override fun call(): MyEntity? {
-        val _cursor: Cursor = query(__db, _statement, false, null)
-        try {
-          val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-          val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
-          val _result: MyEntity?
-          if (_cursor.moveToFirst()) {
-            val _tmpPk: Int
-            _tmpPk = _cursor.getInt(_cursorIndexOfPk)
-            val _tmpOther: String
-            _tmpOther = _cursor.getString(_cursorIndexOfOther)
-            _result = MyEntity(_tmpPk,_tmpOther)
+    return createFlow(__db, false, arrayOf("MyEntity")) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        var _argIndex: Int = 1
+        for (_item: String? in arg) {
+          if (_item == null) {
+            _stmt.bindNull(_argIndex)
           } else {
-            _result = null
+            _stmt.bindText(_argIndex, _item)
           }
-          return _result
-        } finally {
-          _cursor.close()
+          _argIndex++
         }
+        val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+        val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
+        val _result: MyEntity?
+        if (_stmt.step()) {
+          val _tmpPk: Int
+          _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
+          val _tmpOther: String
+          _tmpOther = _stmt.getText(_cursorIndexOfOther)
+          _result = MyEntity(_tmpPk,_tmpOther)
+        } else {
+          _result = null
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-
-      protected fun finalize() {
-        _statement.release()
-      }
-    })
+    }
   }
 
   public override suspend fun getSuspendList(vararg arg: String?): List<MyEntity> {
diff --git a/room/room-migration/api/restricted_current.ignore b/room/room-migration/api/restricted_current.ignore
new file mode 100644
index 0000000..9820ce0
--- /dev/null
+++ b/room/room-migration/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.room.migration.bundle:
+    Removed package androidx.room.migration.bundle
diff --git a/room/room-migration/api/restricted_current.txt b/room/room-migration/api/restricted_current.txt
index 07459f9..e6f50d0 100644
--- a/room/room-migration/api/restricted_current.txt
+++ b/room/room-migration/api/restricted_current.txt
@@ -1,198 +1 @@
 // Signature format: 4.0
-package androidx.room.migration.bundle {
-
-  @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class BundleUtil {
-    method public static String replaceTableName(String contents, String tableName);
-    method public static String replaceViewName(String contents, String viewName);
-    field public static final String TABLE_NAME_PLACEHOLDER = "${TABLE_NAME}";
-    field public static final String VIEW_NAME_PLACEHOLDER = "${VIEW_NAME}";
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DatabaseBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.DatabaseBundle> {
-    ctor @Deprecated public DatabaseBundle();
-    ctor public DatabaseBundle(int version, String identityHash, java.util.List<? extends androidx.room.migration.bundle.EntityBundle> entities, java.util.List<? extends androidx.room.migration.bundle.DatabaseViewBundle> views, java.util.List<java.lang.String> setupQueries);
-    method public java.util.List<java.lang.String> buildCreateQueries();
-    method public java.util.List<androidx.room.migration.bundle.EntityBundle> getEntities();
-    method public java.util.Map<java.lang.String,androidx.room.migration.bundle.EntityBundle> getEntitiesByTableName();
-    method public String getIdentityHash();
-    method public int getVersion();
-    method public java.util.List<androidx.room.migration.bundle.DatabaseViewBundle> getViews();
-    method public final java.util.Map<java.lang.String,androidx.room.migration.bundle.DatabaseViewBundle> getViewsByName();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.DatabaseBundle other);
-    property public java.util.List<androidx.room.migration.bundle.EntityBundle> entities;
-    property public java.util.Map<java.lang.String,androidx.room.migration.bundle.EntityBundle> entitiesByTableName;
-    property public String identityHash;
-    property public int version;
-    property public java.util.List<androidx.room.migration.bundle.DatabaseViewBundle> views;
-    property public final java.util.Map<java.lang.String,androidx.room.migration.bundle.DatabaseViewBundle> viewsByName;
-  }
-
-  public static final class DatabaseBundle.FtsEntityCreateComparator implements java.util.Comparator<androidx.room.migration.bundle.EntityBundle> {
-    ctor public DatabaseBundle.FtsEntityCreateComparator();
-    method public int compare(androidx.room.migration.bundle.EntityBundle firstEntity, androidx.room.migration.bundle.EntityBundle secondEntity);
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DatabaseViewBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.DatabaseViewBundle> {
-    ctor public DatabaseViewBundle(@com.google.gson.annotations.SerializedName("viewName") String viewName, @com.google.gson.annotations.SerializedName("createSql") String createSql);
-    method public String createView();
-    method public String getCreateSql();
-    method public String getViewName();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.DatabaseViewBundle other);
-    property public String createSql;
-    property public String viewName;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class EntityBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.EntityBundle> {
-    ctor public EntityBundle(@com.google.gson.annotations.SerializedName("tableName") String tableName, @com.google.gson.annotations.SerializedName("createSql") String createSql, @com.google.gson.annotations.SerializedName("fields") java.util.List<? extends androidx.room.migration.bundle.FieldBundle> fields, @com.google.gson.annotations.SerializedName("primaryKey") androidx.room.migration.bundle.PrimaryKeyBundle primaryKey, @com.google.gson.annotations.SerializedName("indices") java.util.List<? extends androidx.room.migration.bundle.IndexBundle> indices, @com.google.gson.annotations.SerializedName("foreignKeys") java.util.List<? extends androidx.room.migration.bundle.ForeignKeyBundle> foreignKeys);
-    method public java.util.Collection<java.lang.String> buildCreateQueries();
-    method public String createNewTable();
-    method public String createTable();
-    method public String getCreateSql();
-    method public java.util.List<androidx.room.migration.bundle.FieldBundle> getFields();
-    method public java.util.Map<java.lang.String,androidx.room.migration.bundle.FieldBundle> getFieldsByColumnName();
-    method public java.util.List<androidx.room.migration.bundle.ForeignKeyBundle> getForeignKeys();
-    method public java.util.List<androidx.room.migration.bundle.IndexBundle> getIndices();
-    method public String getNewTableName();
-    method public androidx.room.migration.bundle.PrimaryKeyBundle getPrimaryKey();
-    method public String getTableName();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.EntityBundle other);
-    method public String renameToOriginal();
-    property public String createSql;
-    property public java.util.List<androidx.room.migration.bundle.FieldBundle> fields;
-    property public java.util.Map<java.lang.String,androidx.room.migration.bundle.FieldBundle> fieldsByColumnName;
-    property public java.util.List<androidx.room.migration.bundle.ForeignKeyBundle> foreignKeys;
-    property public java.util.List<androidx.room.migration.bundle.IndexBundle> indices;
-    property public String newTableName;
-    property public androidx.room.migration.bundle.PrimaryKeyBundle primaryKey;
-    property public String tableName;
-    field public static final androidx.room.migration.bundle.EntityBundle.Companion Companion;
-    field public static final String NEW_TABLE_PREFIX = "_new_";
-  }
-
-  public static final class EntityBundle.Companion {
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FieldBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.FieldBundle> {
-    ctor @Deprecated public FieldBundle(String fieldPath, String columnName, String affinity, boolean nonNull);
-    ctor public FieldBundle(@com.google.gson.annotations.SerializedName("fieldPath") String fieldPath, @com.google.gson.annotations.SerializedName("columnName") String columnName, @com.google.gson.annotations.SerializedName("affinity") String affinity, @com.google.gson.annotations.SerializedName("notNull") boolean isNonNull, @com.google.gson.annotations.SerializedName("defaultValue") String? defaultValue);
-    method public String getAffinity();
-    method public String getColumnName();
-    method public String? getDefaultValue();
-    method public String getFieldPath();
-    method public boolean isNonNull();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.FieldBundle other);
-    property public String affinity;
-    property public String columnName;
-    property public String? defaultValue;
-    property public String fieldPath;
-    property public boolean isNonNull;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ForeignKeyBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.ForeignKeyBundle> {
-    ctor public ForeignKeyBundle(String table, String onDelete, String onUpdate, java.util.List<java.lang.String> columns, java.util.List<java.lang.String> referencedColumns);
-    method public java.util.List<java.lang.String> getColumns();
-    method public String getOnDelete();
-    method public String getOnUpdate();
-    method public java.util.List<java.lang.String> getReferencedColumns();
-    method public String getTable();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.ForeignKeyBundle other);
-    property public java.util.List<java.lang.String> columns;
-    property public String onDelete;
-    property public String onUpdate;
-    property public java.util.List<java.lang.String> referencedColumns;
-    property public String table;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FtsEntityBundle extends androidx.room.migration.bundle.EntityBundle {
-    ctor public FtsEntityBundle(String tableName, String createSql, java.util.List<? extends androidx.room.migration.bundle.FieldBundle> fields, androidx.room.migration.bundle.PrimaryKeyBundle primaryKey, String ftsVersion, androidx.room.migration.bundle.FtsOptionsBundle ftsOptions, @com.google.gson.annotations.SerializedName("contentSyncTriggers") java.util.List<java.lang.String> contentSyncSqlTriggers);
-    method public java.util.List<java.lang.String> getContentSyncSqlTriggers();
-    method public androidx.room.migration.bundle.FtsOptionsBundle getFtsOptions();
-    method public String getFtsVersion();
-    method public java.util.List<java.lang.String> getShadowTableNames();
-    property public java.util.List<java.lang.String> contentSyncSqlTriggers;
-    property public androidx.room.migration.bundle.FtsOptionsBundle ftsOptions;
-    property public String ftsVersion;
-    property public java.util.List<java.lang.String> shadowTableNames;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FtsOptionsBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.FtsOptionsBundle> {
-    ctor public FtsOptionsBundle(@com.google.gson.annotations.SerializedName("tokenizer") String tokenizer, @com.google.gson.annotations.SerializedName("tokenizerArgs") java.util.List<java.lang.String> tokenizerArgs, @com.google.gson.annotations.SerializedName("contentTable") String contentTable, @com.google.gson.annotations.SerializedName("languageIdColumnName") String languageIdColumnName, @com.google.gson.annotations.SerializedName("matchInfo") String matchInfo, @com.google.gson.annotations.SerializedName("notIndexedColumns") java.util.List<java.lang.String> notIndexedColumns, @com.google.gson.annotations.SerializedName("prefixSizes") java.util.List<java.lang.Integer> prefixSizes, @com.google.gson.annotations.SerializedName("preferredOrder") String preferredOrder);
-    method public String getContentTable();
-    method public String getLanguageIdColumnName();
-    method public String getMatchInfo();
-    method public java.util.List<java.lang.String> getNotIndexedColumns();
-    method public String getPreferredOrder();
-    method public java.util.List<java.lang.Integer> getPrefixSizes();
-    method public java.util.List<java.lang.String> getTokenizerArgs();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.FtsOptionsBundle other);
-    property public String contentTable;
-    property public String languageIdColumnName;
-    property public String matchInfo;
-    property public java.util.List<java.lang.String> notIndexedColumns;
-    property public String preferredOrder;
-    property public java.util.List<java.lang.Integer> prefixSizes;
-    property public java.util.List<java.lang.String> tokenizerArgs;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class IndexBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.IndexBundle> {
-    ctor @Deprecated public IndexBundle(String name, boolean unique, java.util.List<java.lang.String> columnNames, String createSql);
-    ctor public IndexBundle(@com.google.gson.annotations.SerializedName("name") String name, @com.google.gson.annotations.SerializedName("unique") boolean isUnique, @com.google.gson.annotations.SerializedName("columnNames") java.util.List<java.lang.String>? columnNames, @com.google.gson.annotations.SerializedName("orders") java.util.List<java.lang.String>? orders, @com.google.gson.annotations.SerializedName("createSql") String createSql);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public String create(String tableName);
-    method public java.util.List<java.lang.String>? getColumnNames();
-    method public String getCreateSql();
-    method public String getCreateSql(String tableName);
-    method public String getName();
-    method public java.util.List<java.lang.String>? getOrders();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.IndexBundle other);
-    method public boolean isUnique();
-    property public java.util.List<java.lang.String>? columnNames;
-    property public String createSql;
-    property public boolean isUnique;
-    property public String name;
-    property public java.util.List<java.lang.String>? orders;
-    field public static final androidx.room.migration.bundle.IndexBundle.Companion Companion;
-    field public static final String DEFAULT_PREFIX = "index_";
-  }
-
-  public static final class IndexBundle.Companion {
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PrimaryKeyBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.PrimaryKeyBundle> {
-    ctor public PrimaryKeyBundle(@com.google.gson.annotations.SerializedName("autoGenerate") boolean isAutoGenerate, @com.google.gson.annotations.SerializedName("columnNames") java.util.List<java.lang.String> columnNames);
-    method public java.util.List<java.lang.String> getColumnNames();
-    method public boolean isAutoGenerate();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.PrimaryKeyBundle other);
-    property public java.util.List<java.lang.String> columnNames;
-    property public boolean isAutoGenerate;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SchemaBundle implements androidx.room.migration.bundle.SchemaEquality<androidx.room.migration.bundle.SchemaBundle> {
-    ctor public SchemaBundle(@com.google.gson.annotations.SerializedName("formatVersion") int formatVersion, @com.google.gson.annotations.SerializedName("database") androidx.room.migration.bundle.DatabaseBundle database);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=UnsupportedEncodingException::class) public static final androidx.room.migration.bundle.SchemaBundle deserialize(java.io.InputStream fis) throws java.io.UnsupportedEncodingException;
-    method public androidx.room.migration.bundle.DatabaseBundle getDatabase();
-    method public int getFormatVersion();
-    method public boolean isSchemaEqual(androidx.room.migration.bundle.SchemaBundle other);
-    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static final void serialize(androidx.room.migration.bundle.SchemaBundle bundle, java.io.File file) throws java.io.IOException;
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static final void serialize(androidx.room.migration.bundle.SchemaBundle bundle, java.io.OutputStream outputStream) throws java.io.IOException;
-    property public androidx.room.migration.bundle.DatabaseBundle database;
-    property public int formatVersion;
-    field public static final androidx.room.migration.bundle.SchemaBundle.Companion Companion;
-    field public static final int LATEST_FORMAT = 1; // 0x1
-  }
-
-  public static final class SchemaBundle.Companion {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=UnsupportedEncodingException::class) public androidx.room.migration.bundle.SchemaBundle deserialize(java.io.InputStream fis) throws java.io.UnsupportedEncodingException;
-    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public void serialize(androidx.room.migration.bundle.SchemaBundle bundle, java.io.File file) throws java.io.IOException;
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public void serialize(androidx.room.migration.bundle.SchemaBundle bundle, java.io.OutputStream outputStream) throws java.io.IOException;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class SchemaBundle.EmptySchemaException extends java.lang.IllegalStateException {
-    ctor public SchemaBundle.EmptySchemaException();
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SchemaEquality<T> {
-    method public boolean isSchemaEqual(T other);
-  }
-
-}
-
diff --git a/room/room-migration/build.gradle b/room/room-migration/build.gradle
index 0e7974f..4aed7d4 100644
--- a/room/room-migration/build.gradle
+++ b/room/room-migration/build.gradle
@@ -24,9 +24,11 @@
 
 import androidx.build.PlatformIdentifier
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 
 plugins {
     id("AndroidXPlugin")
+    alias(libs.plugins.kotlinSerialization)
 }
 
 androidXMultiplatform {
@@ -44,6 +46,7 @@
             dependencies {
                 api(libs.kotlinStdlib)
                 implementation(project(":room:room-common"))
+                implementation(libs.kotlinSerializationJson)
             }
         }
         commonTest {
@@ -54,17 +57,25 @@
         }
         jvmMain {
             dependsOn(commonMain)
-            dependencies {
-                implementation(libs.gson)
-            }
         }
         jvmTest {
             dependsOn(commonTest)
             dependencies {
-                implementation(libs.guava)
                 implementation(libs.kotlinTestJunit)
-                implementation(libs.intellijAnnotations)
-                implementation(libs.mockitoCore4)
+            }
+        }
+        nativeMain {
+            dependsOn(commonMain)
+            dependencies {
+                api(libs.okio)
+                implementation(libs.kotlinSerializationJsonOkio)
+            }
+        }
+        targets.all { target ->
+            if (target.platformType == KotlinPlatformType.native) {
+                target.compilations["main"].defaultSourceSet {
+                    dependsOn(nativeMain)
+                }
             }
         }
     }
@@ -75,5 +86,6 @@
     publish = Publish.SNAPSHOT_AND_RELEASE
     inceptionYear = "2017"
     description = "Android Room Migration"
+    legacyDisableKotlinStrictApiMode = true
     metalavaK2UastEnabled = true
 }
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/BaseEntityBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/BaseEntityBundle.kt
new file mode 100644
index 0000000..2e95f76
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/BaseEntityBundle.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.migration.bundle
+
+import androidx.annotation.RestrictTo
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Base class that holds common schema information about an entity.
+ */
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+sealed class BaseEntityBundle {
+    @SerialName("tableName")
+    abstract val tableName: String
+    @SerialName("createSql")
+    abstract val createSql: String
+    @SerialName("fields")
+    abstract val fields: List<FieldBundle>
+    @SerialName("primaryKey")
+    abstract val primaryKey: PrimaryKeyBundle
+    @SerialName("indices")
+    abstract val indices: List<IndexBundle>
+    @SerialName("foreignKeys")
+    abstract val foreignKeys: List<ForeignKeyBundle>
+
+    companion object {
+        const val NEW_TABLE_PREFIX: String = "_new_"
+    }
+
+    val newTableName: String
+        get() {
+            return NEW_TABLE_PREFIX + tableName
+        }
+
+    val fieldsByColumnName: Map<String, FieldBundle> by lazy {
+        fields.associateBy { it.columnName }
+    }
+
+    /**
+     * CREATE TABLE SQL query that uses the actual table name.
+     */
+    fun createTable(): String {
+        return replaceTableName(createSql, tableName)
+    }
+
+    /**
+     * CREATE TABLE SQL query that uses the table name with "new" prefix.
+     */
+    fun createNewTable(): String {
+        return replaceTableName(createSql, newTableName)
+    }
+
+    /**
+     * Renames the table with [newTableName] to [tableName].
+     */
+    fun renameToOriginal(): String {
+        return "ALTER TABLE $newTableName RENAME TO $tableName"
+    }
+
+    /**
+     * Creates the list of SQL queries that are necessary to create this entity.
+     */
+    abstract fun buildCreateQueries(): List<String>
+}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/BundleUtil.jvm.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/BundleUtil.kt
similarity index 73%
rename from room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/BundleUtil.jvm.kt
rename to room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/BundleUtil.kt
index 35aeb14..caaaf5d 100644
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/BundleUtil.jvm.kt
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/BundleUtil.kt
@@ -14,26 +14,27 @@
  * limitations under the License.
  */
 @file:JvmName("BundleUtil")
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 
 package androidx.room.migration.bundle
 
 import androidx.annotation.RestrictTo
+import kotlin.jvm.JvmName
 
 /**
  * Placeholder for table names in queries.
  */
-public const val TABLE_NAME_PLACEHOLDER: String = "\${TABLE_NAME}"
+const val TABLE_NAME_PLACEHOLDER: String = "\${TABLE_NAME}"
 
 /**
  * Placeholder for view names in queries.
  */
-public const val VIEW_NAME_PLACEHOLDER: String = "\${VIEW_NAME}"
+const val VIEW_NAME_PLACEHOLDER: String = "\${VIEW_NAME}"
 
-public fun replaceTableName(contents: String, tableName: String): String {
+fun replaceTableName(contents: String, tableName: String): String {
     return contents.replace(TABLE_NAME_PLACEHOLDER, tableName)
 }
 
-public fun replaceViewName(contents: String, viewName: String): String {
+fun replaceViewName(contents: String, viewName: String): String {
     return contents.replace(VIEW_NAME_PLACEHOLDER, viewName)
 }
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/DatabaseBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/DatabaseBundle.kt
new file mode 100644
index 0000000..13d27a20
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/DatabaseBundle.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import androidx.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality
+import androidx.room.migration.bundle.SchemaEqualityUtil.filterValuesInstance
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class that holds the schema information for a [androidx.room.Database].
+ */
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class DatabaseBundle(
+    @SerialName("version")
+    val version: Int,
+    @SerialName("identityHash")
+    val identityHash: String,
+    @SerialName("entities")
+    val entities: List<BaseEntityBundle>,
+    @SerialName("views")
+    val views: List<DatabaseViewBundle> = emptyList(),
+    @SerialName("setupQueries")
+    private val setupQueries: List<String>,
+) : SchemaEquality<DatabaseBundle> {
+
+    val entitiesByTableName: Map<String, BaseEntityBundle> by lazy {
+        entities.associateBy { it.tableName }
+    }
+
+    val viewsByName: Map<String, DatabaseViewBundle> by lazy {
+        views.associateBy { it.viewName }
+    }
+
+    /**
+     * Builds the list of SQL queries to build this database from scratch.
+     */
+    fun buildCreateQueries(): List<String> {
+        return buildList {
+            entities.sortedWith(FtsEntityCreateComparator()).forEach { entityBundle ->
+                addAll(entityBundle.buildCreateQueries())
+            }
+            views.forEach { viewBundle ->
+                add(viewBundle.createView())
+            }
+            addAll(setupQueries)
+        }
+    }
+
+    override fun isSchemaEqual(other: DatabaseBundle): Boolean {
+        return checkSchemaEquality(
+            entitiesByTableName.filterValuesInstance<String, EntityBundle>(),
+            other.entitiesByTableName.filterValuesInstance<String, EntityBundle>()
+        ) && checkSchemaEquality(
+            entitiesByTableName.filterValuesInstance<String, FtsEntityBundle>(),
+            other.entitiesByTableName.filterValuesInstance<String, FtsEntityBundle>()
+        ) && checkSchemaEquality(viewsByName, other.viewsByName)
+    }
+
+    // Comparator to sort FTS entities after their declared external content entity so that the
+    // content entity table gets created first.
+    private class FtsEntityCreateComparator : Comparator<BaseEntityBundle> {
+        override fun compare(a: BaseEntityBundle, b: BaseEntityBundle): Int {
+            if (a is FtsEntityBundle) {
+                val contentTable = a.ftsOptions.contentTable
+                if (contentTable == b.tableName) {
+                    return 1
+                }
+            } else if (b is FtsEntityBundle) {
+                val contentTable = b.ftsOptions.contentTable
+                if (contentTable == a.tableName) {
+                    return -1
+                }
+            }
+            return 0
+        }
+    }
+}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/DatabaseViewBundle.jvm.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/DatabaseViewBundle.kt
similarity index 65%
rename from room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/DatabaseViewBundle.jvm.kt
rename to room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/DatabaseViewBundle.kt
index dad3cce..0f2344a 100644
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/DatabaseViewBundle.jvm.kt
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/DatabaseViewBundle.kt
@@ -17,28 +17,25 @@
 package androidx.room.migration.bundle
 
 import androidx.annotation.RestrictTo
-import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
 
 /**
  * Data class that holds the schema information about a [androidx.room.DatabaseView].
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class DatabaseViewBundle(
-    @SerializedName("viewName")
-    public open val viewName: String,
-    @SerializedName("createSql")
-    public open val createSql: String
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class DatabaseViewBundle(
+    @SerialName("viewName")
+    val viewName: String,
+    @SerialName("createSql")
+    val createSql: String
 ) : SchemaEquality<DatabaseViewBundle> {
 
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    private constructor() : this("", "")
-
     /**
-     * @return Create view SQL query that uses the actual view name.
+     * CREATE VIEW SQL query that uses the actual view name.
      */
-    public open fun createView(): String {
+    fun createView(): String {
         return replaceViewName(createSql, viewName)
     }
 
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/EntityBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/EntityBundle.kt
new file mode 100644
index 0000000..63921bc
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/EntityBundle.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import androidx.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class that holds the schema information about an [androidx.room.Entity].
+ *
+ */
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+open class EntityBundle(
+    @SerialName("tableName")
+    override val tableName: String,
+    @SerialName("createSql")
+    override val createSql: String,
+    @SerialName("fields")
+    override val fields: List<FieldBundle>,
+    @SerialName("primaryKey")
+    override val primaryKey: PrimaryKeyBundle,
+    @SerialName("indices")
+    override val indices: List<IndexBundle> = emptyList(),
+    @SerialName("foreignKeys")
+    override val foreignKeys: List<ForeignKeyBundle> = emptyList()
+) : BaseEntityBundle(), SchemaEquality<EntityBundle> {
+
+    /**
+     * Creates the list of SQL queries that are necessary to create this entity.
+     */
+    override fun buildCreateQueries(): List<String> {
+        return buildList {
+            add(createTable())
+            [email protected] { indexBundle ->
+                add(indexBundle.create(tableName))
+            }
+        }
+    }
+
+    override fun isSchemaEqual(other: EntityBundle): Boolean {
+        if (tableName != other.tableName) {
+            return false
+        }
+        return checkSchemaEquality(fieldsByColumnName, other.fieldsByColumnName) &&
+            checkSchemaEquality(primaryKey, other.primaryKey) &&
+            checkSchemaEquality(indices, other.indices) &&
+            checkSchemaEquality(foreignKeys, other.foreignKeys)
+    }
+}
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FieldBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FieldBundle.kt
new file mode 100644
index 0000000..eea6dc5
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FieldBundle.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class that holds the schema information for an [androidx.room.Entity] field.
+ */
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class FieldBundle(
+    @SerialName("fieldPath")
+    val fieldPath: String,
+    @SerialName("columnName")
+    val columnName: String,
+    @SerialName("affinity")
+    val affinity: String,
+    @SerialName("notNull")
+    val isNonNull: Boolean = false,
+    @SerialName("defaultValue")
+    val defaultValue: String? = null,
+) : SchemaEquality<FieldBundle> {
+
+    override fun isSchemaEqual(other: FieldBundle): Boolean {
+        if (isNonNull != other.isNonNull) return false
+        if (columnName != other.columnName) {
+            return false
+        }
+        if (defaultValue?.let { it != other.defaultValue } ?: (other.defaultValue != null)) {
+            return false
+        }
+        return affinity == other.affinity
+    }
+}
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/ForeignKeyBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/ForeignKeyBundle.kt
new file mode 100644
index 0000000..745c855
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/ForeignKeyBundle.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class that holds the information about a foreign key reference, i.e.
+ * [androidx.room.ForeignKey].
+ */
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class ForeignKeyBundle(
+    @SerialName("table")
+    val table: String,
+    @SerialName("onDelete")
+    val onDelete: String,
+    @SerialName("onUpdate")
+    val onUpdate: String,
+    @SerialName("columns")
+    val columns: List<String>,
+    @SerialName("referencedColumns")
+    val referencedColumns: List<String>
+) : SchemaEquality<ForeignKeyBundle> {
+
+    override fun isSchemaEqual(other: ForeignKeyBundle): Boolean {
+        if (table != other.table) return false
+        if (onDelete != other.onDelete) return false
+        if (onUpdate != other.onUpdate) return false
+        // order matters
+        return (columns == other.columns && referencedColumns == other.referencedColumns)
+    }
+}
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FtsEntityBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FtsEntityBundle.kt
new file mode 100644
index 0000000..e8a03ea
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FtsEntityBundle.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import androidx.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class that holds the schema information about an [androidx.room.Fts3] or
+ * [androidx.room.Fts4] entity.
+ */
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class FtsEntityBundle(
+    @SerialName("tableName")
+    override val tableName: String,
+    @SerialName("createSql")
+    override val createSql: String,
+    @SerialName("fields")
+    override val fields: List<FieldBundle>,
+    @SerialName("primaryKey")
+    override val primaryKey: PrimaryKeyBundle,
+    @SerialName("indices")
+    override val indices: List<IndexBundle> = emptyList(),
+    @SerialName("foreignKeys")
+    override val foreignKeys: List<ForeignKeyBundle> = emptyList(),
+    @SerialName("ftsVersion")
+    val ftsVersion: String,
+    @SerialName("ftsOptions")
+    val ftsOptions: FtsOptionsBundle,
+    @SerialName("contentSyncTriggers")
+    val contentSyncSqlTriggers: List<String>
+) : BaseEntityBundle(), SchemaEquality<FtsEntityBundle> {
+
+    /**
+     * Creates the list of SQL queries that are necessary to create this entity.
+     */
+    override fun buildCreateQueries(): List<String> {
+        return buildList {
+            add(createTable())
+            addAll(contentSyncSqlTriggers)
+        }
+    }
+
+    override fun isSchemaEqual(other: FtsEntityBundle): Boolean {
+        if (tableName != other.tableName) {
+            return false
+        }
+        return checkSchemaEquality(fieldsByColumnName, other.fieldsByColumnName) &&
+            checkSchemaEquality(primaryKey, other.primaryKey) &&
+            checkSchemaEquality(indices, other.indices) &&
+            checkSchemaEquality(foreignKeys, other.foreignKeys) &&
+            ftsVersion == other.ftsVersion &&
+            checkSchemaEquality(ftsOptions, other.ftsOptions)
+    }
+
+    /**
+     * Gets the list of shadow table names corresponding to the FTS virtual table.
+     */
+    val shadowTableNames: List<String> by lazy {
+        val currentTable = [email protected]
+        buildList {
+            SHADOW_TABLE_NAME_SUFFIXES.forEach { suffix ->
+                add(currentTable + suffix)
+            }
+        }
+    }
+
+    companion object {
+        private val SHADOW_TABLE_NAME_SUFFIXES =
+            listOf(
+                "_content",
+                "_segdir",
+                "_segments",
+                "_stat",
+                "_docsize"
+            )
+    }
+}
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FtsOptionsBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FtsOptionsBundle.kt
new file mode 100644
index 0000000..32a68f8
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/FtsOptionsBundle.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class that holds [androidx.room.FtsOptions] information.
+ */
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class FtsOptionsBundle(
+    @SerialName("tokenizer")
+    private val tokenizer: String,
+    @SerialName("tokenizerArgs")
+    val tokenizerArgs: List<String>,
+    @SerialName("contentTable")
+    val contentTable: String,
+    @SerialName("languageIdColumnName")
+    val languageIdColumnName: String,
+    @SerialName("matchInfo")
+    val matchInfo: String,
+    @SerialName("notIndexedColumns")
+    val notIndexedColumns: List<String>,
+    @SerialName("prefixSizes")
+    val prefixSizes: List<Int>,
+    @SerialName("preferredOrder")
+    val preferredOrder: String
+) : SchemaEquality<FtsOptionsBundle> {
+
+    override fun isSchemaEqual(other: FtsOptionsBundle): Boolean {
+        return tokenizer == other.tokenizer &&
+            tokenizerArgs == other.tokenizerArgs &&
+            contentTable == other.contentTable &&
+            languageIdColumnName == other.languageIdColumnName &&
+            matchInfo == other.matchInfo &&
+            notIndexedColumns == other.notIndexedColumns &&
+            prefixSizes == other.prefixSizes &&
+            preferredOrder == other.preferredOrder
+    }
+}
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/IndexBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/IndexBundle.kt
new file mode 100644
index 0000000..8b1f75e
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/IndexBundle.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import androidx.room.Index
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class that holds the schema information about a table [androidx.room.Index]
+ */
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class IndexBundle(
+    @SerialName("name")
+    val name: String,
+    @SerialName("unique")
+    val isUnique: Boolean,
+    @SerialName("columnNames")
+    val columnNames: List<String>? = null,
+    @SerialName("orders")
+    val orders: List<String>? = null,
+    @SerialName("createSql")
+    val createSql: String
+
+) : SchemaEquality<IndexBundle> {
+    companion object {
+        // should match Index.kt
+        const val DEFAULT_PREFIX: String = "index_"
+    }
+
+    fun create(tableName: String): String {
+        return replaceTableName(createSql, tableName)
+    }
+
+    /**
+     * Gets the CREATE INDEX SQL query that uses the given table name.
+     */
+    fun getCreateSql(tableName: String): String {
+        return replaceTableName(createSql, tableName)
+    }
+
+    override fun isSchemaEqual(other: IndexBundle): Boolean {
+        if (isUnique != other.isUnique) return false
+        if (name.startsWith(DEFAULT_PREFIX)) {
+            if (!other.name.startsWith(DEFAULT_PREFIX)) {
+                return false
+            }
+        } else if (other.name.startsWith(DEFAULT_PREFIX)) {
+            return false
+        } else if (name != other.name) {
+            return false
+        }
+
+        // order matters
+        if (columnNames?.let { columnNames != other.columnNames } ?: (other.columnNames != null)) {
+            return false
+        }
+
+        // order matters and null orders is considered equal to all ASC orders, to be backward
+        // compatible with schemas where orders are not present in the schema file
+        val columnsSize = columnNames?.size ?: 0
+        val orders = if (orders.isNullOrEmpty()) {
+            List(columnsSize) { Index.Order.ASC.name }
+        } else {
+            orders
+        }
+        val otherOrders =
+            if (other.orders.isNullOrEmpty()) {
+                List(columnsSize) { Index.Order.ASC.name }
+            } else {
+                other.orders
+            }
+
+        if (orders != otherOrders) return false
+        return true
+    }
+}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/PrimaryKeyBundle.jvm.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/PrimaryKeyBundle.kt
similarity index 67%
rename from room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/PrimaryKeyBundle.jvm.kt
rename to room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/PrimaryKeyBundle.kt
index 8902985..5f7ae0e 100644
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/PrimaryKeyBundle.jvm.kt
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/PrimaryKeyBundle.kt
@@ -17,22 +17,20 @@
 package androidx.room.migration.bundle
 
 import androidx.annotation.RestrictTo
-import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
 
 /**
  * Data class that holds the schema information about a [androidx.room.PrimaryKey].
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class PrimaryKeyBundle(
-    @SerializedName("autoGenerate")
-    public open val isAutoGenerate: Boolean,
-    @SerializedName("columnNames")
-    public open val columnNames: List<String>
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class PrimaryKeyBundle(
+    @SerialName("autoGenerate")
+    val isAutoGenerate: Boolean,
+    @SerialName("columnNames")
+    val columnNames: List<String>
 ) : SchemaEquality<PrimaryKeyBundle> {
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    private constructor() : this(false, emptyList())
 
     override fun isSchemaEqual(other: PrimaryKeyBundle): Boolean {
         return columnNames == other.columnNames && isAutoGenerate == other.isAutoGenerate
diff --git a/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaBundle.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaBundle.kt
new file mode 100644
index 0000000..8921134
--- /dev/null
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaBundle.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+
+package androidx.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.ClassDiscriminatorMode
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonContentPolymorphicSerializer
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.modules.SerializersModule
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+expect class SchemaBundle(
+    formatVersion: Int,
+    database: DatabaseBundle
+) : SchemaEquality<SchemaBundle> {
+
+    val formatVersion: Int
+    val database: DatabaseBundle
+
+    override fun isSchemaEqual(other: SchemaBundle): Boolean
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+const val SCHEMA_LATEST_FORMAT_VERSION = 1
+
+@OptIn(ExperimentalSerializationApi::class) // due to prettyPrintIndex
+internal val json = Json {
+    // The schema files are meant to be human readable and are checked-in into repositories.
+    prettyPrint = true
+    // Keep index to 2 spaces as that is what we used before kotlinx-serialization
+    prettyPrintIndent = "  "
+    // Don't output class discriminator as that would encode library class names into JSON file
+    // making implementation details harder to refactor. When reading, we use a content inspector
+    // that will perform polymorphic deserialization.
+    classDiscriminatorMode = ClassDiscriminatorMode.NONE
+    serializersModule = SerializersModule {
+        polymorphicDefaultDeserializer(BaseEntityBundle::class) { EntitySerializer }
+    }
+}
+
+private object EntitySerializer : JsonContentPolymorphicSerializer<BaseEntityBundle>(
+    baseClass = BaseEntityBundle::class
+) {
+    override fun selectDeserializer(
+        element: JsonElement
+    ): DeserializationStrategy<BaseEntityBundle> = when {
+        "ftsVersion" in element.jsonObject -> FtsEntityBundle.serializer()
+        else -> EntityBundle.serializer()
+    }
+}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaEquality.jvm.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaEquality.kt
similarity index 85%
rename from room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaEquality.jvm.kt
rename to room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaEquality.kt
index 702dca7..2901924 100644
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaEquality.jvm.kt
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaEquality.kt
@@ -21,9 +21,8 @@
 /**
  * A loose equals check which checks schema equality instead of 100% equality (e.g. order of
  * columns in an entity does not have to match)
- *
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public interface SchemaEquality<T> {
-    public fun isSchemaEqual(other: T): Boolean
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface SchemaEquality<T> {
+    fun isSchemaEqual(other: T): Boolean
 }
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaEqualityUtil.jvm.kt b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaEqualityUtil.kt
similarity index 80%
rename from room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaEqualityUtil.jvm.kt
rename to room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaEqualityUtil.kt
index ef98f44..28bb731 100644
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaEqualityUtil.jvm.kt
+++ b/room/room-migration/src/commonMain/kotlin/androidx/room/migration/bundle/SchemaEqualityUtil.kt
@@ -16,14 +16,11 @@
 
 package androidx.room.migration.bundle
 
-import androidx.annotation.RestrictTo
-
 /**
  * Utility class to run schema equality on collections.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public object SchemaEqualityUtil {
-    public fun <T, K : SchemaEquality<K>> checkSchemaEquality(
+internal object SchemaEqualityUtil {
+    fun <T, K : SchemaEquality<K>> checkSchemaEquality(
         map1: Map<T, K>?,
         map2: Map<T, K>?
     ): Boolean {
@@ -37,7 +34,7 @@
         }
     }
 
-    public fun <K : SchemaEquality<K>> checkSchemaEquality(
+    fun <K : SchemaEquality<K>> checkSchemaEquality(
         list1: List<K>?,
         list2: List<K>?
     ): Boolean {
@@ -53,8 +50,7 @@
         }
     }
 
-    @SuppressWarnings("SimplifiableIfStatement")
-    public fun <K : SchemaEquality<K>> checkSchemaEquality(
+    fun <K : SchemaEquality<K>> checkSchemaEquality(
         item1: K?,
         item2: K?
     ): Boolean {
@@ -64,4 +60,8 @@
             else -> item1.isSchemaEqual(item2)
         }
     }
+
+    inline fun <K, reified R> Map<K, *>.filterValuesInstance(): Map<K, R> = buildMap {
+        [email protected] { (key, value) -> if (value is R) put(key, value) }
+    }
 }
diff --git a/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/DatabaseBundleTest.kt b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/DatabaseBundleTest.kt
new file mode 100644
index 0000000..d869647
--- /dev/null
+++ b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/DatabaseBundleTest.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.room.migration.bundle
+
+import androidx.kruth.assertThat
+import kotlin.test.Test
+
+class DatabaseBundleTest {
+
+    @Test
+    fun buildCreateQueries_noFts() {
+        val entity1 = EntityBundle(
+            tableName = "e1", createSql = "sq1",
+            fields = listOf(createFieldBundle("foo1"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo1")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+        val entity2 = EntityBundle(
+            tableName = "e2", createSql = "sq2",
+            fields = listOf(createFieldBundle("foo2"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo2")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+        val bundle = DatabaseBundle(
+            version = 1, identityHash = "hash",
+            entities = listOf(entity1, entity2), views = emptyList(),
+            setupQueries = emptyList()
+        )
+
+        assertThat(bundle.buildCreateQueries()).containsExactly("sq1", "sq2")
+    }
+
+    @Test
+    fun buildCreateQueries_withFts() {
+        val entity1 = EntityBundle(
+            tableName = "e1", createSql = "sq1",
+            fields = listOf(createFieldBundle("foo1"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo1")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+        val entity2 = FtsEntityBundle(
+            tableName = "e2", createSql = "sq2",
+            fields = listOf(createFieldBundle("foo2"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo2")),
+            ftsVersion = "FTS4",
+            ftsOptions = createFtsOptionsBundle(""),
+            contentSyncSqlTriggers = emptyList()
+        )
+        val entity3 = EntityBundle(
+            tableName = "e3", createSql = "sq3",
+            fields = listOf(createFieldBundle("foo3"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo3")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+        val bundle = DatabaseBundle(
+            version = 1, identityHash = "hash",
+            entities = listOf(entity1, entity2, entity3), views = emptyList(),
+            setupQueries = emptyList()
+        )
+
+        assertThat(bundle.buildCreateQueries()).containsExactly("sq1", "sq2", "sq3")
+    }
+
+    @Test
+    fun buildCreateQueries_withExternalContentFts() {
+        val entity1 = EntityBundle(
+            tableName = "e1", createSql = "sq1",
+            fields = listOf(createFieldBundle("foo1"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo1")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+        val entity2 = FtsEntityBundle(
+            tableName = "e2", createSql = "sq2",
+            fields = listOf(createFieldBundle("foo2"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo2")),
+            ftsVersion = "FTS4",
+            ftsOptions = createFtsOptionsBundle("e3"),
+            contentSyncSqlTriggers = listOf("e2_trig")
+        )
+        val entity3 = EntityBundle(
+            tableName = "e3", createSql = "sq3",
+            fields = listOf(createFieldBundle("foo3"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo3")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+        val bundle = DatabaseBundle(
+            version = 1,
+            identityHash = "hash",
+            entities = listOf(entity1, entity2, entity3),
+            views = emptyList(),
+            setupQueries = emptyList()
+        )
+
+        assertThat(bundle.buildCreateQueries()).containsExactly("sq1", "sq3", "sq2", "e2_trig")
+    }
+
+    @Test
+    fun schemaEquality_missingView_notEqual() {
+        val entity = EntityBundle(
+            tableName = "e", createSql = "sq",
+            fields = listOf(createFieldBundle("foo"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+        val view = DatabaseViewBundle(viewName = "bar", createSql = "sq")
+        val bundle1 = DatabaseBundle(
+            version = 1, identityHash = "hash",
+            entities = listOf(entity), views = emptyList(),
+            setupQueries = emptyList()
+        )
+        val bundle2 = DatabaseBundle(
+            version = 1, identityHash = "hash",
+            entities = listOf(entity), views = listOf(view),
+            setupQueries = emptyList()
+        )
+        assertThat(bundle1.isSchemaEqual(bundle2)).isFalse()
+    }
+
+    private fun createFieldBundle(name: String): FieldBundle {
+        return FieldBundle(
+            fieldPath = "foo",
+            columnName = name,
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+    }
+
+    private fun createFtsOptionsBundle(contentTableName: String): FtsOptionsBundle {
+        return FtsOptionsBundle(
+            tokenizer = "",
+            tokenizerArgs = emptyList(),
+            contentTable = contentTableName,
+            languageIdColumnName = "",
+            matchInfo = "",
+            notIndexedColumns = emptyList(),
+            prefixSizes = emptyList(),
+            preferredOrder = ""
+        )
+    }
+}
diff --git a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/DatabaseViewBundleTest.kt b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/DatabaseViewBundleTest.kt
similarity index 61%
rename from room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/DatabaseViewBundleTest.kt
rename to room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/DatabaseViewBundleTest.kt
index 75fb554..224e54e 100644
--- a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/DatabaseViewBundleTest.kt
+++ b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/DatabaseViewBundleTest.kt
@@ -16,20 +16,16 @@
 
 package androidx.room.migration.bundle
 
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
 
-@RunWith(JUnit4::class)
 class DatabaseViewBundleTest {
     @Test
     fun basic() {
-        val bundle = DatabaseViewBundle("abc", "def")
-        val other = DatabaseViewBundle("abc", "def")
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-        assertThat(bundle.viewName, `is`("abc"))
-        assertThat(bundle.createSql, `is`("def"))
+        val bundle = DatabaseViewBundle(viewName = "abc", createSql = "def")
+        val other = DatabaseViewBundle(viewName = "abc", createSql = "def")
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+        assertThat(bundle.viewName).isEqualTo("abc")
+        assertThat(bundle.createSql).isEqualTo("def")
     }
 }
diff --git a/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/EntityBundleTest.kt b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/EntityBundleTest.kt
new file mode 100644
index 0000000..2b1ef7c
--- /dev/null
+++ b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/EntityBundleTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.kruth.assertThat
+import kotlin.test.Test
+
+class EntityBundleTest {
+    @Test
+    fun schemaEquality_same_equal() {
+        val bundle = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = listOf(createFieldBundle("foo"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = listOf(createIndexBundle("foo")),
+            foreignKeys = listOf(createForeignKeyBundle("bar", "foo"))
+        )
+
+        val other = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = listOf(createFieldBundle("foo"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = listOf(createIndexBundle("foo")),
+            foreignKeys = listOf(createForeignKeyBundle("bar", "foo"))
+        )
+
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_reorderedFields_equal() {
+        val bundle = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = listOf(createFieldBundle("foo"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+
+        val other = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = listOf(createFieldBundle("bar"), createFieldBundle("foo")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffFields_notEqual() {
+        val bundle = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = listOf(createFieldBundle("foo"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+
+        val other = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = listOf(createFieldBundle("foo2"), createFieldBundle("bar")),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = emptyList()
+        )
+
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_reorderedForeignKeys_equal() {
+        val bundle = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = emptyList(),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = listOf(
+                createForeignKeyBundle("x", "y"),
+                createForeignKeyBundle("bar", "foo")
+            )
+        )
+
+        val other = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = emptyList(),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = listOf(
+                createForeignKeyBundle("bar", "foo"),
+                createForeignKeyBundle("x", "y")
+            )
+        )
+
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffForeignKeys_notEqual() {
+        val bundle = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = emptyList(),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = listOf(createForeignKeyBundle("bar", "foo"))
+        )
+
+        val other = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = emptyList(),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = emptyList(),
+            foreignKeys = listOf(createForeignKeyBundle("bar2", "foo"))
+        )
+
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_reorderedIndices_equal() {
+        val bundle = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = emptyList(),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = listOf(createIndexBundle("foo"), createIndexBundle("baz")),
+            foreignKeys = emptyList()
+        )
+
+        val other = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = emptyList(),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = listOf(createIndexBundle("baz"), createIndexBundle("foo")),
+            foreignKeys = emptyList()
+        )
+
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffIndices_notEqual() {
+        val bundle = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = emptyList(),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = listOf(createIndexBundle("foo")),
+            foreignKeys = emptyList()
+        )
+
+        val other = EntityBundle(
+            tableName = "foo", createSql = "sq",
+            fields = emptyList(),
+            primaryKey = PrimaryKeyBundle(false, listOf("foo")),
+            indices = listOf(createIndexBundle("foo2")),
+            foreignKeys = emptyList()
+        )
+
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    private fun createFieldBundle(name: String): FieldBundle {
+        return FieldBundle(
+            fieldPath = "foo",
+            columnName = name,
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+    }
+
+    private fun createIndexBundle(colName: String): IndexBundle {
+        return IndexBundle(
+            name = "ind_$colName", isUnique = false,
+            columnNames = listOf(colName), orders = emptyList(), createSql = "create"
+        )
+    }
+
+    private fun createForeignKeyBundle(targetTable: String, column: String): ForeignKeyBundle {
+        return ForeignKeyBundle(
+            table = targetTable, onDelete = "CASCADE", onUpdate = "CASCADE",
+            columns = listOf(column), referencedColumns = listOf(column)
+        )
+    }
+}
diff --git a/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/FieldBundleTest.kt b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/FieldBundleTest.kt
new file mode 100644
index 0000000..a3b9918
--- /dev/null
+++ b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/FieldBundleTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.kruth.assertThat
+import kotlin.test.Test
+
+class FieldBundleTest {
+    @Test
+    fun schemaEquality_same_equal() {
+        val bundle = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+        val copy = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+        assertThat(bundle.isSchemaEqual(copy)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffNonNull_notEqual() {
+        val bundle = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+        val copy = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = true,
+            defaultValue = null
+        )
+        assertThat(bundle.isSchemaEqual(copy)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffColumnName_notEqual() {
+        val bundle = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+        val copy = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo2",
+            affinity = "text",
+            isNonNull = true,
+            defaultValue = null
+        )
+        assertThat(bundle.isSchemaEqual(copy)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffAffinity_notEqual() {
+        val bundle = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+        val copy = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo2",
+            affinity = "int",
+            isNonNull = false,
+            defaultValue = null
+        )
+        assertThat(bundle.isSchemaEqual(copy)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffPath_equal() {
+        val bundle = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+        val copy = FieldBundle(
+            fieldPath = "foo>bar",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = false,
+            defaultValue = null
+        )
+        assertThat(bundle.isSchemaEqual(copy)).isTrue()
+    }
+
+    @Test
+    fun schemeEquality_diffDefaultValue_notEqual() {
+        val bundle = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = true,
+            defaultValue = null
+        )
+        val copy = FieldBundle(
+            fieldPath = "foo",
+            columnName = "foo",
+            affinity = "text",
+            isNonNull = true,
+            defaultValue = "bar"
+        )
+        assertThat(bundle.isSchemaEqual(copy)).isFalse()
+    }
+}
diff --git a/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/ForeignKeyBundleTest.kt b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/ForeignKeyBundleTest.kt
new file mode 100644
index 0000000..0263e0c
--- /dev/null
+++ b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/ForeignKeyBundleTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.kruth.assertThat
+import kotlin.test.Test
+
+class ForeignKeyBundleTest {
+    @Test
+    fun schemaEquality_same_equal() {
+        val bundle = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        val other = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffTable_notEqual() {
+        val bundle = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        val other = ForeignKeyBundle(
+            table = "table2", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffOnDelete_notEqual() {
+        val bundle = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete2",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        val other = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffOnUpdate_notEqual() {
+        val bundle = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        val other = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate2", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffSrcOrder_notEqual() {
+        val bundle = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col2", "col1"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        val other = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffTargetOrder_notEqual() {
+        val bundle = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target1", "target2")
+        )
+        val other = ForeignKeyBundle(
+            table = "table", onDelete = "onDelete",
+            onUpdate = "onUpdate", columns = listOf("col1", "col2"),
+            referencedColumns = listOf("target2", "target1")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+}
diff --git a/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/IndexBundleTest.kt b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/IndexBundleTest.kt
new file mode 100644
index 0000000..be09d5b
--- /dev/null
+++ b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/IndexBundleTest.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.kruth.assertThat
+import kotlin.test.Test
+
+class IndexBundleTest {
+    @Test
+    fun schemaEquality_same_equal() {
+        val bundle = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        val other = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffName_notEqual() {
+        val bundle = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        val other = IndexBundle(
+            name = "index3", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffGenericName_equal() {
+        val bundle = IndexBundle(
+            name = IndexBundle.DEFAULT_PREFIX + "x", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        val other = IndexBundle(
+            name = IndexBundle.DEFAULT_PREFIX + "y", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffUnique_notEqual() {
+        val bundle = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        val other = IndexBundle(
+            name = "index1", isUnique = true,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffColumns_notEqual() {
+        val bundle = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        val other = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col2", "col1"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffSql_equal() {
+        val bundle = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        val other = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql22"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffSort_notEqual() {
+        val bundle = IndexBundle(
+            "index1", false,
+            listOf("col1", "col2"), listOf("ASC", "DESC"), "sql"
+        )
+        val other = IndexBundle(
+            "index1", false,
+            listOf("col1", "col2"), listOf("DESC", "ASC"), "sql"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_sortNullVsAllAsc_isEqual() {
+        val bundle = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        val other = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = null, createSql = "sql"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_sortEmptyVsAllAsc_isEqual() {
+        val bundle = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = listOf("ASC", "ASC"), createSql = "sql"
+        )
+        val other = IndexBundle(
+            name = "index1", isUnique = false,
+            columnNames = listOf("col1", "col2"), orders = emptyList(), createSql = "sql"
+        )
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+}
diff --git a/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/PrimaryKeyBundleTest.kt b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/PrimaryKeyBundleTest.kt
new file mode 100644
index 0000000..accd6f1
--- /dev/null
+++ b/room/room-migration/src/commonTest/kotlin/androidx/room/migration/bundle/PrimaryKeyBundleTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.kruth.assertThat
+import kotlin.test.Test
+
+class PrimaryKeyBundleTest {
+    @Test
+    fun schemaEquality_same_equal() {
+        val bundle = PrimaryKeyBundle(
+            isAutoGenerate = true,
+            columnNames = listOf("foo", "bar")
+        )
+        val other = PrimaryKeyBundle(
+            isAutoGenerate = true,
+            columnNames = listOf("foo", "bar")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isTrue()
+    }
+
+    @Test
+    fun schemaEquality_diffAutoGen_notEqual() {
+        val bundle = PrimaryKeyBundle(
+            isAutoGenerate = true,
+            columnNames = listOf("foo", "bar")
+        )
+        val other = PrimaryKeyBundle(
+            isAutoGenerate = false,
+            columnNames = listOf("foo", "bar")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffColumns_notEqual() {
+        val bundle = PrimaryKeyBundle(
+            isAutoGenerate = true,
+            columnNames = listOf("foo", "baz")
+        )
+        val other = PrimaryKeyBundle(
+            isAutoGenerate = true,
+            columnNames = listOf("foo", "bar")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+
+    @Test
+    fun schemaEquality_diffColumnOrder_notEqual() {
+        val bundle = PrimaryKeyBundle(
+            isAutoGenerate = true,
+            columnNames = listOf("foo", "bar")
+        )
+        val other = PrimaryKeyBundle(
+            isAutoGenerate = true,
+            columnNames = listOf("bar", "foo")
+        )
+        assertThat(bundle.isSchemaEqual(other)).isFalse()
+    }
+}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/DatabaseBundle.jvm.kt b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/DatabaseBundle.jvm.kt
deleted file mode 100644
index 3c620b1..0000000
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/DatabaseBundle.jvm.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import androidx.annotation.RestrictTo
-import androidx.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality
-import com.google.gson.annotations.SerializedName
-
-/**
- * Data class that holds the schema information for a [androidx.room.Database].
- *
- * @property version Version
- * @property identityHash Identity hash
- * @property entities List of entities
- * @property views List of views
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class DatabaseBundle(
-    @field:SerializedName("version")
-    public open val version: Int,
-    @field:SerializedName("identityHash")
-    public open val identityHash: String,
-    @field:SerializedName("entities")
-    public open val entities: List<EntityBundle>,
-    @field:SerializedName("views")
-    public open val views: List<DatabaseViewBundle>,
-    @field:SerializedName("setupQueries")
-    private val setupQueries: List<String>,
-) : SchemaEquality<DatabaseBundle> {
-
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    public constructor() : this(0, "", emptyList(), emptyList(), emptyList())
-
-    @delegate:Transient
-    public open val entitiesByTableName: Map<String, EntityBundle> by lazy {
-        entities.associateBy { it.tableName }
-    }
-
-    @delegate:Transient
-    public val viewsByName: Map<String, DatabaseViewBundle> by lazy {
-        views.associateBy { it.viewName }
-    }
-
-    /**
-     * @return List of SQL queries to build this database from scratch.
-     */
-    public open fun buildCreateQueries(): List<String> {
-        return buildList {
-            entities.sortedWith(FtsEntityCreateComparator()).forEach { entityBundle ->
-                addAll(entityBundle.buildCreateQueries())
-            }
-            views.forEach { viewBundle ->
-                add(viewBundle.createView())
-            }
-            addAll(setupQueries)
-        }
-    }
-
-    @Override
-    override fun isSchemaEqual(other: DatabaseBundle): Boolean {
-        return checkSchemaEquality(entitiesByTableName, other.entitiesByTableName) &&
-            checkSchemaEquality(viewsByName, other.viewsByName)
-    }
-
-    // Comparator to sort FTS entities after their declared external content entity so that the
-    // content entity table gets created first.
-    public class FtsEntityCreateComparator : Comparator<EntityBundle> {
-        override fun compare(firstEntity: EntityBundle, secondEntity: EntityBundle): Int {
-            if (firstEntity is FtsEntityBundle) {
-                val contentTable = firstEntity.ftsOptions.contentTable
-                if (contentTable == secondEntity.tableName) {
-                    return 1
-                }
-            } else if (secondEntity is FtsEntityBundle) {
-                val contentTable = secondEntity.ftsOptions.contentTable
-                if (contentTable == firstEntity.tableName) {
-                    return -1
-                }
-            }
-            return 0
-        }
-    }
-}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/EntityBundle.jvm.kt b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/EntityBundle.jvm.kt
deleted file mode 100644
index 958c918..0000000
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/EntityBundle.jvm.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import androidx.annotation.RestrictTo
-import androidx.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality
-import com.google.gson.annotations.SerializedName
-
-/**
- * Data class that holds the schema information about an [androidx.room.Entity].
- *
- * @property tableName The table name.
- * @property createSql Create query with the table name placeholder.
- * @property fields The list of fields.
- * @property primaryKey The primary key.
- * @property indices The list of indices
- * @property foreignKeys The list of foreign keys
- *
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class EntityBundle(
-    @SerializedName("tableName")
-    public open val tableName: String,
-    @SerializedName("createSql")
-    public open val createSql: String,
-    @SerializedName("fields")
-    public open val fields: List<FieldBundle>,
-    @SerializedName("primaryKey")
-    public open val primaryKey: PrimaryKeyBundle,
-    @SerializedName("indices")
-    public open val indices: List<IndexBundle>,
-    @SerializedName("foreignKeys")
-    public open val foreignKeys: List<ForeignKeyBundle>
-) : SchemaEquality<EntityBundle> {
-
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    private constructor() : this(
-        "",
-        "",
-        emptyList(),
-        PrimaryKeyBundle(false, emptyList()),
-        emptyList(),
-        emptyList()
-    )
-
-    public companion object {
-        public const val NEW_TABLE_PREFIX: String = "_new_"
-    }
-
-    public open val newTableName: String
-        get() {
-            return NEW_TABLE_PREFIX + tableName
-        }
-
-    @delegate:Transient
-    public open val fieldsByColumnName: Map<String, FieldBundle> by lazy {
-        fields.associateBy { it.columnName }
-    }
-
-    /**
-     * @return Create table SQL query that uses the actual table name.
-     */
-    public open fun createTable(): String {
-        return replaceTableName(createSql, tableName)
-    }
-
-    /**
-     * @return Create table SQL query that uses the table name with "new" prefix.
-     */
-    public open fun createNewTable(): String {
-        return replaceTableName(createSql, newTableName)
-    }
-
-    /**
-     * @return Renames the table with [newTableName] to [tableName].
-     */
-    public open fun renameToOriginal(): String {
-        return "ALTER TABLE $newTableName RENAME TO $tableName"
-    }
-
-    /**
-     * @return Creates the list of SQL queries that are necessary to create this entity.
-     */
-    public open fun buildCreateQueries(): Collection<String> {
-        return buildList {
-            add(createTable())
-            [email protected] { indexBundle ->
-                add(indexBundle.create(tableName))
-            }
-        }
-    }
-
-    override fun isSchemaEqual(other: EntityBundle): Boolean {
-        if (tableName != other.tableName) {
-            return false
-        }
-        return checkSchemaEquality(
-            fieldsByColumnName,
-            other.fieldsByColumnName
-        ) &&
-            checkSchemaEquality(primaryKey, other.primaryKey) &&
-            checkSchemaEquality(indices, other.indices) &&
-            checkSchemaEquality(foreignKeys, other.foreignKeys)
-    }
-}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FieldBundle.jvm.kt b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FieldBundle.jvm.kt
deleted file mode 100644
index 758b8e39..0000000
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FieldBundle.jvm.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import androidx.annotation.RestrictTo
-import com.google.gson.annotations.SerializedName
-
-/**
- * Data class that holds the schema information for an [androidx.room.Entity] field.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class FieldBundle(
-    @SerializedName("fieldPath")
-    public open val fieldPath: String,
-    @SerializedName("columnName")
-    public open val columnName: String,
-    @SerializedName("affinity")
-    public open val affinity: String,
-    @SerializedName("notNull")
-    public open val isNonNull: Boolean,
-    @SerializedName("defaultValue")
-    public open val defaultValue: String?,
-) : SchemaEquality<FieldBundle> {
-
-    @Deprecated("Use [FieldBundle(String, String, String, boolean, String)")
-    public constructor(fieldPath: String, columnName: String, affinity: String, nonNull: Boolean) :
-        this(fieldPath, columnName, affinity, nonNull, null)
-
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    private constructor() : this("", "", "", false, null)
-
-    override fun isSchemaEqual(other: FieldBundle): Boolean {
-        if (isNonNull != other.isNonNull) return false
-        if (columnName != other.columnName) {
-            return false
-        }
-        if (defaultValue?.let { it != other.defaultValue } ?: (other.defaultValue != null)) {
-            return false
-        }
-        return affinity == other.affinity
-    }
-}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/ForeignKeyBundle.jvm.kt b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/ForeignKeyBundle.jvm.kt
deleted file mode 100644
index 3e8c079..0000000
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/ForeignKeyBundle.jvm.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import androidx.annotation.RestrictTo
-import com.google.gson.annotations.SerializedName
-
-/**
- * Data class that holds the information about a foreign key reference, i.e.
- * [androidx.room.ForeignKey].
- *
- * @property table             The target table
- * @property onDelete          OnDelete action
- * @property onUpdate          OnUpdate action
- * @property columns           The list of columns in the current table
- * @property referencedColumns The list of columns in the referenced table
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class ForeignKeyBundle(
-    @field:SerializedName("table")
-    public open val table: String,
-    @field:SerializedName("onDelete")
-    public open val onDelete: String,
-    @field:SerializedName("onUpdate")
-    public open val onUpdate: String,
-    @field:SerializedName("columns")
-    public open val columns: List<String>,
-    @field:SerializedName("referencedColumns")
-    public open val referencedColumns: List<String>
-) : SchemaEquality<ForeignKeyBundle> {
-
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    private constructor() : this("", "", "", emptyList(), emptyList())
-
-    override fun isSchemaEqual(other: ForeignKeyBundle): Boolean {
-        if (table != other.table) return false
-        if (onDelete != other.onDelete) return false
-        if (onUpdate != other.onUpdate) return false
-        // order matters
-        return (columns == other.columns && referencedColumns == other.referencedColumns)
-    }
-}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FtsEntityBundle.jvm.kt b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FtsEntityBundle.jvm.kt
deleted file mode 100644
index 266f776..0000000
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FtsEntityBundle.jvm.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import androidx.annotation.RestrictTo
-import androidx.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality
-import com.google.gson.annotations.SerializedName
-
-/**
- * Data class that holds the schema information about an [androidx.room.Fts3] or
- * [androidx.room.Fts4] entity.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class FtsEntityBundle(
-    tableName: String,
-    createSql: String,
-    fields: List<FieldBundle>,
-    primaryKey: PrimaryKeyBundle,
-    @field:SerializedName("ftsVersion")
-    public open val ftsVersion: String,
-    @field:SerializedName("ftsOptions")
-    public open val ftsOptions: FtsOptionsBundle,
-    @SerializedName("contentSyncTriggers")
-    public open val contentSyncSqlTriggers: List<String>
-) : EntityBundle(
-    tableName,
-    createSql,
-    fields,
-    primaryKey,
-    emptyList(),
-    emptyList()
-) {
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    private constructor() : this(
-        "",
-        "",
-        emptyList(),
-        PrimaryKeyBundle(false, emptyList()),
-        "",
-        FtsOptionsBundle("", emptyList(), "", "", "", emptyList(), emptyList(), ""),
-        emptyList()
-    )
-
-    @Transient
-    private val SHADOW_TABLE_NAME_SUFFIXES = listOf(
-        "_content",
-        "_segdir",
-        "_segments",
-        "_stat",
-        "_docsize"
-    )
-
-    /**
-     * @return Creates the list of SQL queries that are necessary to create this entity.
-     */
-   override fun buildCreateQueries(): Collection<String> {
-        return buildList {
-            add(createTable())
-            addAll(contentSyncSqlTriggers)
-        }
-    }
-
-    override fun isSchemaEqual(other: EntityBundle): Boolean {
-        val isSuperSchemaEqual = super.isSchemaEqual(other)
-        return if (other is FtsEntityBundle) {
-            isSuperSchemaEqual && ftsVersion == other.ftsVersion &&
-                checkSchemaEquality(ftsOptions, other.ftsOptions)
-        } else {
-            isSuperSchemaEqual
-        }
-    }
-
-    /**
-     * Gets the list of shadow table names corresponding to the FTS virtual table.
-     * @return the list of names.
-     */
-    @delegate:Transient
-    public open val shadowTableNames: List<String> by lazy {
-        val currentTable = [email protected]
-        buildList {
-            SHADOW_TABLE_NAME_SUFFIXES.forEach { suffix ->
-                add(currentTable + suffix)
-            }
-        }
-    }
-}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FtsOptionsBundle.jvm.kt b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FtsOptionsBundle.jvm.kt
deleted file mode 100644
index e73c374..0000000
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/FtsOptionsBundle.jvm.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import androidx.annotation.RestrictTo
-import com.google.gson.annotations.SerializedName
-
-/**
- * Data class that holds [androidx.room.FtsOptions] information.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class FtsOptionsBundle(
-    @SerializedName("tokenizer")
-    private val tokenizer: String,
-    @SerializedName("tokenizerArgs")
-    public open val tokenizerArgs: List<String>,
-    @SerializedName("contentTable")
-    public open val contentTable: String,
-    @SerializedName("languageIdColumnName")
-    public open val languageIdColumnName: String,
-    @SerializedName("matchInfo")
-    public open val matchInfo: String,
-    @SerializedName("notIndexedColumns")
-    public open val notIndexedColumns: List<String>,
-    @SerializedName("prefixSizes")
-    public open val prefixSizes: List<Int>,
-    @SerializedName("preferredOrder")
-    public open val preferredOrder: String
-) : SchemaEquality<FtsOptionsBundle> {
-
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    private constructor() : this("", emptyList(), "", "", "", emptyList(), emptyList(), "")
-
-    override fun isSchemaEqual(other: FtsOptionsBundle): Boolean {
-        return tokenizer == other.tokenizer &&
-            tokenizerArgs == other.tokenizerArgs &&
-            contentTable == other.contentTable &&
-            languageIdColumnName == other.languageIdColumnName &&
-            matchInfo == other.matchInfo &&
-            notIndexedColumns == other.notIndexedColumns &&
-            prefixSizes == other.prefixSizes &&
-            preferredOrder == other.preferredOrder
-    }
-}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/IndexBundle.jvm.kt b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/IndexBundle.jvm.kt
deleted file mode 100644
index b21f9a5..0000000
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/IndexBundle.jvm.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import androidx.annotation.RestrictTo
-import androidx.room.Index
-import com.google.gson.annotations.SerializedName
-
-/**
- * Data class that holds the schema information about a table [androidx.room.Index]
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class IndexBundle(
-    @SerializedName("name")
-    public open val name: String,
-    @SerializedName("unique")
-    public open val isUnique: Boolean,
-    @SerializedName("columnNames")
-    public open val columnNames: List<String>?,
-    @SerializedName("orders")
-    public open val orders: List<String>?,
-    @SerializedName("createSql")
-    public open val createSql: String
-
-) : SchemaEquality<IndexBundle> {
-    public companion object {
-        // should match Index.kt
-        public const val DEFAULT_PREFIX: String = "index_"
-    }
-
-    /**
-     * @deprecated Use {@link #IndexBundle(String, boolean, List, List, String)}
-     */
-    @Deprecated("Use {@link #IndexBundle(String, boolean, List, List, String)}")
-    public constructor(
-        name: String,
-        unique: Boolean,
-        columnNames: List<String>,
-        createSql: String
-    ) : this(name, unique, columnNames, null, createSql)
-
-    // Used by GSON
-    @Deprecated("Marked deprecated to avoid usage in the codebase")
-    @SuppressWarnings("unused")
-    private constructor() : this("", false, emptyList(), emptyList(), "")
-
-    /**
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    public open fun create(tableName: String): String {
-        return replaceTableName(createSql, tableName)
-    }
-
-    /**
-     * @param tableName The table name.
-     * @return Create index SQL query that uses the given table name.
-     */
-    public open fun getCreateSql(tableName: String): String {
-        return replaceTableName(createSql, tableName)
-    }
-
-    override fun isSchemaEqual(other: IndexBundle): Boolean {
-        if (isUnique != other.isUnique) return false
-        if (name.startsWith(DEFAULT_PREFIX)) {
-            if (!other.name.startsWith(DEFAULT_PREFIX)) {
-                return false
-            }
-        } else if (other.name.startsWith(DEFAULT_PREFIX)) {
-            return false
-        } else if (!name.equals(other.name)) {
-            return false
-        }
-
-        // order matters
-        if (columnNames?.let { columnNames != other.columnNames } ?: (other.columnNames != null)) {
-            return false
-        }
-
-        // order matters and null orders is considered equal to all ASC orders, to be backward
-        // compatible with schemas where orders are not present in the schema file
-        val columnsSize = columnNames?.size ?: 0
-        val orders = if (orders.isNullOrEmpty()) {
-            List(columnsSize) { Index.Order.ASC.name }
-        } else {
-            orders
-        }
-        val otherOrders =
-            if (other.orders.isNullOrEmpty()) {
-                List(columnsSize) { Index.Order.ASC.name }
-            } else {
-                other.orders
-            }
-
-        if (orders != otherOrders) return false
-        return true
-    }
-}
diff --git a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaBundle.jvm.kt b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaBundle.jvm.kt
index 5c0e92f..7a46bf5 100644
--- a/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaBundle.jvm.kt
+++ b/room/room-migration/src/jvmMain/kotlin/androidx/room/migration/bundle/SchemaBundle.jvm.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 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.
@@ -17,129 +17,42 @@
 package androidx.room.migration.bundle
 
 import androidx.annotation.RestrictTo
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import com.google.gson.TypeAdapter
-import com.google.gson.TypeAdapterFactory
-import com.google.gson.annotations.SerializedName
-import com.google.gson.reflect.TypeToken
-import com.google.gson.stream.JsonReader
-import com.google.gson.stream.JsonWriter
-import java.io.File
-import java.io.FileOutputStream
-import java.io.IOException
 import java.io.InputStream
-import java.io.InputStreamReader
 import java.io.OutputStream
-import java.io.OutputStreamWriter
-import java.io.UnsupportedEncodingException
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.decodeFromStream
+import kotlinx.serialization.json.encodeToStream
 
 /**
  * Data class that holds the information about a database schema export.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public open class SchemaBundle(
-    @SerializedName("formatVersion")
-    public open val formatVersion: Int,
-    @SerializedName("database")
-    public open val database: DatabaseBundle
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+actual class SchemaBundle actual constructor(
+    @SerialName("formatVersion")
+    actual val formatVersion: Int,
+    @SerialName("database")
+    actual val database: DatabaseBundle
 ) : SchemaEquality<SchemaBundle> {
-    public companion object {
-        private const val CHARSET = "UTF-8"
-        public const val LATEST_FORMAT: Int = 1
-        private val GSON: Gson = GsonBuilder()
-            .setPrettyPrinting()
-            .disableHtmlEscaping()
-            .registerTypeAdapterFactory(
-                EntityTypeAdapterFactory()
-            )
-            .create()
 
-        @Throws(UnsupportedEncodingException::class)
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-        @JvmStatic
-        public fun deserialize(fis: InputStream): SchemaBundle {
-            InputStreamReader(fis, CHARSET).use { inputStream ->
-                return GSON.fromJson(inputStream, SchemaBundle::class.javaObjectType)
-                    ?: throw EmptySchemaException()
-            }
+    actual override fun isSchemaEqual(other: SchemaBundle): Boolean {
+        return formatVersion == other.formatVersion &&
+            SchemaEqualityUtil.checkSchemaEquality(database, other.database)
+    }
+
+    companion object {
+        @OptIn(ExperimentalSerializationApi::class) // For decodeFromStream() with InputStream
+        fun deserialize(fis: InputStream): SchemaBundle = fis.use {
+            json.decodeFromStream(it)
         }
 
-        @Throws(IOException::class)
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-        @JvmStatic
-        @Deprecated("Prefer overload version that has OutputStream as parameter.")
-        public fun serialize(bundle: SchemaBundle, file: File) {
-            serialize(bundle, FileOutputStream(file, false))
-        }
-
-        @Throws(IOException::class)
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-        @JvmStatic
-        public fun serialize(bundle: SchemaBundle, outputStream: OutputStream) {
-            OutputStreamWriter(outputStream, CHARSET).use { outputStreamWriter ->
-                GSON.toJson(bundle, outputStreamWriter)
+        @OptIn(ExperimentalSerializationApi::class) // For encodeToStream() with OutputStream
+        fun serialize(bundle: SchemaBundle, outputStream: OutputStream) {
+            outputStream.use {
+                json.encodeToStream(bundle, it)
             }
         }
     }
-
-    override fun isSchemaEqual(other: SchemaBundle): Boolean {
-        return SchemaEqualityUtil.checkSchemaEquality(database, other.database) &&
-            formatVersion == other.formatVersion
-    }
-
-    private open class EntityTypeAdapterFactory : TypeAdapterFactory {
-        @Suppress("UNCHECKED_CAST")
-        override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
-            if (!EntityBundle::class.java.isAssignableFrom(type.rawType)) {
-                return null
-            }
-            val jsonElementAdapter = gson.getAdapter(
-                JsonElement::class.java
-            )
-            val entityBundleAdapter = gson.getDelegateAdapter(
-                this,
-                TypeToken.get(EntityBundle::class.java)
-            )
-            val ftsEntityBundleAdapter = gson.getDelegateAdapter(
-                this,
-                TypeToken.get(FtsEntityBundle::class.java)
-            )
-            return EntityTypeAdapter(
-                jsonElementAdapter, entityBundleAdapter, ftsEntityBundleAdapter
-            ) as TypeAdapter<T>
-        }
-
-        private class EntityTypeAdapter(
-            val jsonElementAdapter: TypeAdapter<JsonElement>,
-            val entityBundleAdapter: TypeAdapter<EntityBundle>,
-            val ftsEntityBundleAdapter: TypeAdapter<FtsEntityBundle>
-        ) : TypeAdapter<EntityBundle>() {
-            @Throws(IOException::class)
-            override fun write(out: JsonWriter?, value: EntityBundle?) {
-                if (value is FtsEntityBundle) {
-                    ftsEntityBundleAdapter.write(out, value)
-                } else {
-                    entityBundleAdapter.write(out, value)
-                }
-            }
-
-            override fun read(input: JsonReader?): EntityBundle {
-                val jsonObject: JsonObject = jsonElementAdapter.read(input).asJsonObject
-                return if (jsonObject.has("ftsVersion")) {
-                    ftsEntityBundleAdapter.fromJsonTree(jsonObject)
-                } else {
-                    entityBundleAdapter.fromJsonTree(jsonObject)
-                }
-            }
-        }
-    }
-
-    /**
-     * A exception indicating a schema file being read was empty.
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    public class EmptySchemaException : IllegalStateException("Empty schema file")
 }
diff --git a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/DatabaseBundleTest.kt b/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/DatabaseBundleTest.kt
deleted file mode 100644
index 7726687..0000000
--- a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/DatabaseBundleTest.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class DatabaseBundleTest {
-
-    @Test
-    fun buildCreateQueries_noFts() {
-        val entity1 = EntityBundle("e1", "sq1",
-                listOf(createFieldBundle("foo1"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo1")),
-                emptyList(),
-                emptyList())
-        val entity2 = EntityBundle("e2", "sq2",
-            listOf(createFieldBundle("foo2"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo2")),
-                emptyList(),
-                emptyList())
-        val bundle = DatabaseBundle(1, "hash",
-            listOf(entity1, entity2), emptyList(),
-                emptyList())
-
-        assertThat(bundle.buildCreateQueries(), `is`(listOf("sq1", "sq2")))
-    }
-
-    @Test
-    fun buildCreateQueries_withFts() {
-        val entity1 = EntityBundle("e1", "sq1",
-            listOf(createFieldBundle("foo1"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo1")),
-                emptyList(),
-                emptyList())
-        val entity2 = FtsEntityBundle("e2", "sq2",
-            listOf(createFieldBundle("foo2"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo2")),
-                "FTS4",
-                createFtsOptionsBundle(""),
-                emptyList())
-        val entity3 = EntityBundle("e3", "sq3",
-            listOf(createFieldBundle("foo3"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo3")),
-                emptyList(),
-                emptyList())
-        val bundle = DatabaseBundle(1, "hash",
-            listOf(entity1, entity2, entity3), emptyList(),
-                emptyList())
-
-        assertThat(bundle.buildCreateQueries(), `is`(listOf("sq1", "sq2", "sq3")))
-    }
-
-    @Test
-    fun buildCreateQueries_withExternalContentFts() {
-        val entity1 = EntityBundle("e1", "sq1",
-            listOf(createFieldBundle("foo1"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo1")),
-                emptyList(),
-                emptyList())
-        val entity2 = FtsEntityBundle("e2", "sq2",
-            listOf(createFieldBundle("foo2"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo2")),
-                "FTS4",
-                createFtsOptionsBundle("e3"),
-            listOf("e2_trig"))
-        val entity3 = EntityBundle("e3", "sq3",
-        listOf(createFieldBundle("foo3"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo3")),
-                emptyList(),
-                emptyList())
-        val bundle = DatabaseBundle(
-            1,
-            "hash",
-            listOf(entity1, entity2, entity3),
-            emptyList(),
-            emptyList()
-        )
-
-        assertThat(bundle.buildCreateQueries(), `is`(listOf("sq1", "sq3", "sq2", "e2_trig")))
-    }
-
-    @Test
-    fun schemaEquality_missingView_notEqual() {
-        val entity = EntityBundle("e", "sq",
-            listOf(createFieldBundle("foo"), createFieldBundle("bar")),
-            PrimaryKeyBundle(false, listOf("foo")),
-            emptyList(),
-            emptyList())
-        val view = DatabaseViewBundle("bar", "sq")
-        val bundle1 = DatabaseBundle(1, "hash",
-            listOf(entity), emptyList(),
-            emptyList())
-        val bundle2 = DatabaseBundle(1, "hash",
-            listOf(entity), listOf(view),
-            emptyList())
-        assertThat(bundle1.isSchemaEqual(bundle2), `is`(false))
-    }
-
-    private fun createFieldBundle(name: String): FieldBundle {
-        return FieldBundle("foo", name, "text", false, null)
-    }
-
-    private fun createFtsOptionsBundle(contentTableName: String): FtsOptionsBundle {
-        return FtsOptionsBundle("", emptyList(), contentTableName,
-                "", "", emptyList(), emptyList(), "")
-    }
-}
diff --git a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/EntityBundleTest.kt b/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/EntityBundleTest.kt
deleted file mode 100644
index 53e2b1e..0000000
--- a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/EntityBundleTest.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
-@RunWith(JUnit4::class)
-class EntityBundleTest {
-    @Test
-    fun schemaEquality_same_equal() {
-        val bundle = EntityBundle("foo", "sq",
-                listOf(createFieldBundle("foo"), createFieldBundle("bar")),
-            PrimaryKeyBundle(false, listOf("foo")),
-        listOf(createIndexBundle("foo")),
-        listOf(createForeignKeyBundle("bar", "foo")))
-
-        val other = EntityBundle("foo", "sq",
-            listOf(createFieldBundle("foo"), createFieldBundle("bar")),
-            PrimaryKeyBundle(false, listOf("foo")),
-        listOf(createIndexBundle("foo")),
-        listOf(createForeignKeyBundle("bar", "foo")))
-
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_reorderedFields_equal() {
-        val bundle = EntityBundle("foo", "sq",
-            listOf(createFieldBundle("foo"), createFieldBundle("bar")),
-            PrimaryKeyBundle(false, listOf("foo")),
-                emptyList(),
-                emptyList())
-
-        val other = EntityBundle("foo", "sq",
-            listOf(createFieldBundle("bar"), createFieldBundle("foo")),
-            PrimaryKeyBundle(false, listOf("foo")),
-                emptyList(),
-                emptyList())
-
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffFields_notEqual() {
-        val bundle = EntityBundle("foo", "sq",
-            listOf(createFieldBundle("foo"), createFieldBundle("bar")),
-                PrimaryKeyBundle(false, listOf("foo")),
-                emptyList(),
-                emptyList())
-
-        val other = EntityBundle("foo", "sq",
-            listOf(createFieldBundle("foo2"), createFieldBundle("bar")),
-            PrimaryKeyBundle(false, listOf("foo")),
-                emptyList(),
-                emptyList())
-
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_reorderedForeignKeys_equal() {
-        val bundle = EntityBundle("foo", "sq",
-                emptyList(),
-                PrimaryKeyBundle(false, listOf("foo")),
-                emptyList(),
-            listOf(createForeignKeyBundle("x", "y"),
-                        createForeignKeyBundle("bar", "foo")))
-
-        val other = EntityBundle("foo", "sq",
-                emptyList(),
-                PrimaryKeyBundle(false, listOf("foo")),
-                emptyList(),
-            listOf(createForeignKeyBundle("bar", "foo"),
-                        createForeignKeyBundle("x", "y")))
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffForeignKeys_notEqual() {
-        val bundle = EntityBundle("foo", "sq",
-                emptyList(),
-                PrimaryKeyBundle(false, listOf("foo")),
-                emptyList(),
-            listOf(createForeignKeyBundle("bar", "foo")))
-
-        val other = EntityBundle("foo", "sq",
-                emptyList(),
-            PrimaryKeyBundle(false, listOf("foo")),
-                emptyList(),
-            listOf(createForeignKeyBundle("bar2", "foo")))
-
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_reorderedIndices_equal() {
-        val bundle = EntityBundle("foo", "sq",
-                emptyList(),
-                PrimaryKeyBundle(false, listOf("foo")),
-            listOf(createIndexBundle("foo"), createIndexBundle("baz")),
-                emptyList())
-
-        val other = EntityBundle("foo", "sq",
-                emptyList(),
-                PrimaryKeyBundle(false, listOf("foo")),
-            listOf(createIndexBundle("baz"), createIndexBundle("foo")),
-                emptyList())
-
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffIndices_notEqual() {
-        val bundle = EntityBundle("foo", "sq",
-                emptyList(),
-                PrimaryKeyBundle(false, listOf("foo")),
-            listOf(createIndexBundle("foo")),
-                emptyList())
-
-        val other = EntityBundle("foo", "sq",
-                emptyList(),
-                PrimaryKeyBundle(false, listOf("foo")),
-            listOf(createIndexBundle("foo2")),
-                emptyList())
-
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    private fun createFieldBundle(name: String): FieldBundle {
-        return FieldBundle("foo", name, "text", false, null)
-    }
-
-    private fun createIndexBundle(colName: String): IndexBundle {
-        return IndexBundle(
-            "ind_$colName", false,
-            listOf(colName), emptyList(), "create")
-    }
-
-    private fun createForeignKeyBundle(targetTable: String, column: String): ForeignKeyBundle {
-        return ForeignKeyBundle(targetTable, "CASCADE", "CASCADE",
-            listOf(column), listOf(column))
-    }
-}
diff --git a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/FieldBundleTest.kt b/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/FieldBundleTest.kt
deleted file mode 100644
index 0dccf46..0000000
--- a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/FieldBundleTest.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class FieldBundleTest {
-    @Test
-    fun schemaEquality_same_equal() {
-        val bundle = FieldBundle("foo", "foo", "text", false, null)
-        val copy = FieldBundle("foo", "foo", "text", false, null)
-        assertThat(bundle.isSchemaEqual(copy), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffNonNull_notEqual() {
-        val bundle = FieldBundle("foo", "foo", "text", false, null)
-        val copy = FieldBundle("foo", "foo", "text", true, null)
-        assertThat(bundle.isSchemaEqual(copy), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffColumnName_notEqual() {
-        val bundle = FieldBundle("foo", "foo", "text", false, null)
-        val copy = FieldBundle("foo", "foo2", "text", true, null)
-        assertThat(bundle.isSchemaEqual(copy), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffAffinity_notEqual() {
-        val bundle = FieldBundle("foo", "foo", "text", false, null)
-        val copy = FieldBundle("foo", "foo2", "int", false, null)
-        assertThat(bundle.isSchemaEqual(copy), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffPath_equal() {
-        val bundle = FieldBundle("foo", "foo", "text", false, null)
-        val copy = FieldBundle("foo>bar", "foo", "text", false, null)
-        assertThat(bundle.isSchemaEqual(copy), `is`(true))
-    }
-
-    @Test
-    fun schemeEquality_diffDefaultValue_notEqual() {
-        val bundle = FieldBundle("foo", "foo", "text", true, null)
-        val copy = FieldBundle("foo", "foo", "text", true, "bar")
-        assertThat(bundle.isSchemaEqual(copy), `is`(false))
-    }
-}
diff --git a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/ForeignKeyBundleTest.kt b/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/ForeignKeyBundleTest.kt
deleted file mode 100644
index fd9e8e1..0000000
--- a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/ForeignKeyBundleTest.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ForeignKeyBundleTest {
-    @Test
-    fun schemaEquality_same_equal() {
-        val bundle = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-                listOf("target1", "target2")
-        )
-        val other = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffTable_notEqual() {
-        val bundle = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        val other = ForeignKeyBundle("table2", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffOnDelete_notEqual() {
-        val bundle = ForeignKeyBundle("table", "onDelete2",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        val other = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffOnUpdate_notEqual() {
-        val bundle = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        val other = ForeignKeyBundle("table", "onDelete",
-                "onUpdate2", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffSrcOrder_notEqual() {
-        val bundle = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col2", "col1"),
-            listOf("target1", "target2"))
-        val other = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffTargetOrder_notEqual() {
-        val bundle = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target1", "target2"))
-        val other = ForeignKeyBundle("table", "onDelete",
-                "onUpdate", listOf("col1", "col2"),
-            listOf("target2", "target1"))
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-}
diff --git a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/IndexBundleTest.kt b/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/IndexBundleTest.kt
deleted file mode 100644
index a6451ce..0000000
--- a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/IndexBundleTest.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class IndexBundleTest {
-    @Test
-    fun schemaEquality_same_equal() {
-        val bundle = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        val other = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffName_notEqual() {
-        val bundle = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        val other = IndexBundle("index3", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffGenericName_equal() {
-        val bundle = IndexBundle(IndexBundle.DEFAULT_PREFIX + "x", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        val other = IndexBundle(IndexBundle.DEFAULT_PREFIX + "y", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffUnique_notEqual() {
-        val bundle = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        val other = IndexBundle("index1", true,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffColumns_notEqual() {
-        val bundle = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        val other = IndexBundle("index1", false,
-            listOf("col2", "col1"), listOf("ASC", "ASC"), "sql")
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffSql_equal() {
-        val bundle = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        val other = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql22")
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffSort_notEqual() {
-        val bundle = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "DESC"), "sql")
-        val other = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("DESC", "ASC"), "sql")
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_sortNullVsAllAsc_isEqual() {
-        val bundle = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        val other = IndexBundle("index1", false,
-            listOf("col1", "col2"), null, "sql")
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_sortEmptyVsAllAsc_isEqual() {
-        val bundle = IndexBundle("index1", false,
-            listOf("col1", "col2"), listOf("ASC", "ASC"), "sql")
-        val other = IndexBundle("index1", false,
-            listOf("col1", "col2"), emptyList(), "sql")
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-}
diff --git a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/PrimaryKeyBundleTest.kt b/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/PrimaryKeyBundleTest.kt
deleted file mode 100644
index 8b43988..0000000
--- a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/PrimaryKeyBundleTest.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.migration.bundle
-
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class PrimaryKeyBundleTest {
-    @Test
-    fun schemaEquality_same_equal() {
-        val bundle = PrimaryKeyBundle(true,
-                listOf("foo", "bar")
-        )
-        val other = PrimaryKeyBundle(true,
-            listOf("foo", "bar"))
-        assertThat(bundle.isSchemaEqual(other), `is`(true))
-    }
-
-    @Test
-    fun schemaEquality_diffAutoGen_notEqual() {
-        val bundle = PrimaryKeyBundle(true,
-            listOf("foo", "bar"))
-        val other = PrimaryKeyBundle(false,
-            listOf("foo", "bar"))
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-    @Test
-    fun schemaEquality_diffColumns_notEqual() {
-        val bundle = PrimaryKeyBundle(true,
-            listOf("foo", "baz"))
-        val other = PrimaryKeyBundle(true,
-            listOf("foo", "bar"))
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-
-   @Test
-   fun schemaEquality_diffColumnOrder_notEqual() {
-        val bundle = PrimaryKeyBundle(true,
-            listOf("foo", "bar"))
-        val other = PrimaryKeyBundle(true,
-            listOf("bar", "foo"))
-        assertThat(bundle.isSchemaEqual(other), `is`(false))
-    }
-}
diff --git a/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/SerializationTest.kt b/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/SerializationTest.kt
new file mode 100644
index 0000000..ab2a049
--- /dev/null
+++ b/room/room-migration/src/jvmTest/kotlin/androidx/room/migration/bundle/SerializationTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.migration.bundle
+
+import androidx.kruth.assertThrows
+import java.io.ByteArrayInputStream
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.nio.file.Path
+import kotlin.io.path.Path
+import kotlin.io.path.inputStream
+import kotlin.test.Test
+import kotlinx.serialization.SerializationException
+
+class SerializationTest {
+
+    @Test
+    fun emptyStream() {
+        // GSON had a specific exception for an empty file, but with Kotlin serialization it is
+        // as any other serialization exception. We have a test for this since we expect the
+        // exception to properly report an error in the annotation processor.
+        assertThrows<SerializationException> {
+            SchemaBundle.deserialize(ByteArrayInputStream(byteArrayOf()))
+        }
+    }
+
+    @Test
+    fun fileNotFound() {
+        // This is mostly validating File streams throwing FileNotFoundException, but we expect
+        // such exception when the schema file does not exist to properly report an error
+        // in the annotation processor.
+        assertThrows<FileNotFoundException> {
+            FileInputStream("/fake/file/path").use { SchemaBundle.deserialize(it) }
+        }
+    }
+
+    /**
+     * Validates old schemas that didn't have [FieldBundle.isNonNull] ('notNull'),
+     * added in ag/2579620
+     */
+    @Test
+    fun missingFieldBundleNotNull() {
+        getSchemaPath("missing_field_notnull").inputStream().use {
+            SchemaBundle.deserialize(it)
+        }
+    }
+
+    /**
+     * Validates old schemas that didn't have [DatabaseBundle.views] ('views'),
+     * added in aosp/731045
+     */
+    @Test
+    fun missingDatabaseBundleViews() {
+        getSchemaPath("missing_database_views").inputStream().use {
+            SchemaBundle.deserialize(it)
+        }
+    }
+
+    /**
+     * Validates old schemas that didn't have [FieldBundle.defaultValue] ('defaultValue'),
+     * added in aosp/825803
+     */
+    @Test
+    fun missingFieldBundleDefaultValue() {
+        getSchemaPath("missing_field_defaultvalue").inputStream().use {
+            SchemaBundle.deserialize(it)
+        }
+    }
+
+    /**
+     * Validates old schemas that didn't have [IndexBundle.orders] ('orders'),
+     * added in aosp/1707963
+     */
+    @Test
+    fun missingIndexBundleOrders() {
+        getSchemaPath("missing_index_orders").inputStream().use {
+            SchemaBundle.deserialize(it)
+        }
+    }
+
+    private fun getSchemaPath(name: String): Path {
+        return Path("src/jvmTest/test-data/$name.json")
+    }
+}
diff --git a/room/room-migration/src/jvmTest/test-data/missing_database_views.json b/room/room-migration/src/jvmTest/test-data/missing_database_views.json
new file mode 100644
index 0000000..52eb9ac
--- /dev/null
+++ b/room/room-migration/src/jvmTest/test-data/missing_database_views.json
@@ -0,0 +1,34 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "12345",
+    "entities": [
+      {
+        "tableName": "Entity",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))",
+        "fields": [
+          {
+            "fieldPath": "pk",
+            "columnName": "pk",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "pk"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12345')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/room-migration/src/jvmTest/test-data/missing_field_defaultvalue.json b/room/room-migration/src/jvmTest/test-data/missing_field_defaultvalue.json
new file mode 100644
index 0000000..52eb9ac
--- /dev/null
+++ b/room/room-migration/src/jvmTest/test-data/missing_field_defaultvalue.json
@@ -0,0 +1,34 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "12345",
+    "entities": [
+      {
+        "tableName": "Entity",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))",
+        "fields": [
+          {
+            "fieldPath": "pk",
+            "columnName": "pk",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "pk"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12345')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/room-migration/src/jvmTest/test-data/missing_field_notnull.json b/room/room-migration/src/jvmTest/test-data/missing_field_notnull.json
new file mode 100644
index 0000000..05bda24
--- /dev/null
+++ b/room/room-migration/src/jvmTest/test-data/missing_field_notnull.json
@@ -0,0 +1,32 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "12345",
+    "entities": [
+      {
+        "tableName": "Entity",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk` INTEGER, PRIMARY KEY(`pk`))",
+        "fields": [
+          {
+            "fieldPath": "pk",
+            "columnName": "pk",
+            "affinity": "INTEGER"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "pk"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12345')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/room-migration/src/jvmTest/test-data/missing_index_orders.json b/room/room-migration/src/jvmTest/test-data/missing_index_orders.json
new file mode 100644
index 0000000..7a8ff87
--- /dev/null
+++ b/room/room-migration/src/jvmTest/test-data/missing_index_orders.json
@@ -0,0 +1,42 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "12345",
+    "entities": [
+      {
+        "tableName": "Entity",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk` INTEGER, PRIMARY KEY(`pk`))",
+        "fields": [
+          {
+            "fieldPath": "pk",
+            "columnName": "pk",
+            "affinity": "INTEGER"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "pk"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_AutoMigrationEntity_data",
+            "unique": false,
+            "columnNames": [
+              "data"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Entity_data` ON `${TABLE_NAME}` (`data`)"
+          }
+        ],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12345')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/room-migration/src/nativeMain/kotlin/androidx/room/migration/bundle/SchemaBundle.native.kt b/room/room-migration/src/nativeMain/kotlin/androidx/room/migration/bundle/SchemaBundle.native.kt
new file mode 100644
index 0000000..bb9dfd3
--- /dev/null
+++ b/room/room-migration/src/nativeMain/kotlin/androidx/room/migration/bundle/SchemaBundle.native.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.migration.bundle
+
+import androidx.annotation.RestrictTo
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.okio.decodeFromBufferedSource
+import kotlinx.serialization.json.okio.encodeToBufferedSink
+import okio.BufferedSink
+import okio.BufferedSource
+
+@Serializable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+actual class SchemaBundle actual constructor(
+    @SerialName("formatVersion")
+    actual val formatVersion: Int,
+    @SerialName("database")
+    actual val database: DatabaseBundle
+) : SchemaEquality<SchemaBundle> {
+
+    actual override fun isSchemaEqual(other: SchemaBundle): Boolean {
+        return formatVersion == other.formatVersion &&
+            SchemaEqualityUtil.checkSchemaEquality(database, other.database)
+    }
+
+    companion object {
+        @OptIn(ExperimentalSerializationApi::class) // For decodeFromBufferedSource
+        fun deserialize(source: BufferedSource): SchemaBundle =
+            json.decodeFromBufferedSource(source)
+
+        @OptIn(ExperimentalSerializationApi::class) // For encodeToBufferedSink
+        fun serialize(bundle: SchemaBundle, sink: BufferedSink) {
+            json.encodeToBufferedSink(bundle, sink)
+        }
+    }
+}
diff --git a/room/room-paging/src/androidTest/kotlin/androidx/room/InvalidationTrackerExtRoomPaging.kt b/room/room-paging/src/androidTest/kotlin/androidx/room/InvalidationTrackerExtRoomPaging.kt
deleted file mode 100644
index f97ed38..0000000
--- a/room/room-paging/src/androidTest/kotlin/androidx/room/InvalidationTrackerExtRoomPaging.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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 java.util.concurrent.TimeUnit
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.withTimeout
-
-/**
- * True if invalidation tracker is pending a refresh event to get database changes.
- */
-val InvalidationTracker.pendingRefreshForTest
-    get() = this.pendingRefresh.get()
-
-/**
- * Polls [InvalidationTracker] until it sets its pending refresh flag to true.
- */
-suspend fun InvalidationTracker.awaitPendingRefresh() {
-    withTimeout(TimeUnit.SECONDS.toMillis(3)) {
-        while (true) {
-            if (pendingRefreshForTest) return@withTimeout
-            delay(50)
-        }
-    }
-}
diff --git a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index 739624f..65518e8 100644
--- a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -21,13 +21,11 @@
 import androidx.kruth.assertThat
 import androidx.paging.PagingConfig
 import androidx.paging.PagingSource
-import androidx.paging.PagingSource.LoadParams
 import androidx.paging.PagingSource.LoadResult
 import androidx.paging.testing.TestPager
 import androidx.room.Room
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
-import androidx.room.awaitPendingRefresh
 import androidx.room.util.getColumnIndexOrThrow
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -760,15 +758,15 @@
         )
 
         // blocks invalidation notification from Room
-        queryExecutor.filterFunction = { runnable ->
-            runnable !== db.invalidationTracker.refreshRunnable
+        queryExecutor.filterFunction = {
+            // TODO(b/): Avoid relying on function name, very brittle.
+            !it.toString().contains("refreshInvalidationAsync")
         }
 
         // now write to the database
         dao.deleteTestItem(ITEMS_LIST[30])
 
         // make sure room requests a refresh
-        db.invalidationTracker.awaitPendingRefresh()
         // and that this is blocked to simulate delayed notification from room
         queryExecutor.awaitDeferredSizeAtLeast(1)
 
@@ -791,15 +789,14 @@
         )
 
         // blocks invalidation notification from Room
-        queryExecutor.filterFunction = { runnable ->
-            runnable !== db.invalidationTracker.refreshRunnable
+        queryExecutor.filterFunction = {
+            !it.toString().contains("refreshInvalidationAsync")
         }
 
         // now write to the database
         dao.deleteTestItem(ITEMS_LIST[30])
 
         // make sure room requests a refresh
-        db.invalidationTracker.awaitPendingRefresh()
         // and that this is blocked to simulate delayed notification from room
         queryExecutor.awaitDeferredSizeAtLeast(1)
 
diff --git a/room/room-paging/src/main/java/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/main/java/androidx/room/paging/LimitOffsetPagingSource.kt
index eae82a6..c8326e9 100644
--- a/room/room-paging/src/main/java/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/main/java/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -23,7 +23,6 @@
 import androidx.paging.PagingState
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
-import androidx.room.getQueryDispatcher
 import androidx.room.paging.util.INITIAL_ITEM_COUNT
 import androidx.room.paging.util.INVALID
 import androidx.room.paging.util.ThreadSafeInvalidationObserver
@@ -67,7 +66,7 @@
     )
 
     override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {
-        return withContext(db.getQueryDispatcher()) {
+        return withContext(db.getQueryContext()) {
             observer.registerIfNecessary(db)
             val tempCount = itemCount.get()
             // if itemCount is < 0, then it is initial load
diff --git a/room/room-runtime/api/current.txt b/room/room-runtime/api/current.txt
index b035080..fe0f7f8 100644
--- a/room/room-runtime/api/current.txt
+++ b/room/room-runtime/api/current.txt
@@ -18,6 +18,7 @@
     field public final boolean multiInstanceInvalidation;
     field public final String? name;
     field public final androidx.room.RoomDatabase.PrepackagedDatabaseCallback? prepackagedDatabaseCallback;
+    field public final kotlin.coroutines.CoroutineContext? queryCoroutineContext;
     field public final java.util.concurrent.Executor queryExecutor;
     field public final boolean requireMigration;
     field public final androidx.sqlite.SQLiteDriver? sqliteDriver;
@@ -125,6 +126,7 @@
     method public androidx.room.RoomDatabase.Builder<T> setJournalMode(androidx.room.RoomDatabase.JournalMode journalMode);
     method @SuppressCompatibility @androidx.room.ExperimentalRoomApi public androidx.room.RoomDatabase.Builder<T> setMultiInstanceInvalidationServiceIntent(android.content.Intent invalidationServiceIntent);
     method public androidx.room.RoomDatabase.Builder<T> setQueryCallback(androidx.room.RoomDatabase.QueryCallback queryCallback, java.util.concurrent.Executor executor);
+    method public final androidx.room.RoomDatabase.Builder<T> setQueryCoroutineContext(kotlin.coroutines.CoroutineContext context);
     method public androidx.room.RoomDatabase.Builder<T> setQueryExecutor(java.util.concurrent.Executor executor);
     method public androidx.room.RoomDatabase.Builder<T> setTransactionExecutor(java.util.concurrent.Executor executor);
   }
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 741ef7f..1c74435 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -2,15 +2,15 @@
 package androidx.room {
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class CoroutinesRoom {
-    method public static <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
-    method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+    method @Deprecated public static <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
+    method @Deprecated public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
     method @Deprecated public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
     field public static final androidx.room.CoroutinesRoom.Companion Companion;
   }
 
   public static final class CoroutinesRoom.Companion {
-    method public <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
-    method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+    method @Deprecated public <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
+    method @Deprecated public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
     method @Deprecated public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
   }
 
@@ -18,7 +18,7 @@
     ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, boolean requireMigration, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom);
     ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, android.content.Intent? multiInstanceInvalidationServiceIntent, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom, String? copyFromAssetPath, java.io.File? copyFromFile, java.util.concurrent.Callable<java.io.InputStream>? copyFromInputStream, androidx.room.RoomDatabase.PrepackagedDatabaseCallback? prepackagedDatabaseCallback, java.util.List<?> typeConverters, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
     ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, android.content.Intent? multiInstanceInvalidationServiceIntent, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom, String? copyFromAssetPath, java.io.File? copyFromFile, java.util.concurrent.Callable<java.io.InputStream>? copyFromInputStream, androidx.room.RoomDatabase.PrepackagedDatabaseCallback? prepackagedDatabaseCallback, java.util.List<?> typeConverters, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs, boolean allowDestructiveMigrationForAllTables);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory? sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, android.content.Intent? multiInstanceInvalidationServiceIntent, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom, String? copyFromAssetPath, java.io.File? copyFromFile, java.util.concurrent.Callable<java.io.InputStream>? copyFromInputStream, androidx.room.RoomDatabase.PrepackagedDatabaseCallback? prepackagedDatabaseCallback, java.util.List<?> typeConverters, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs, boolean allowDestructiveMigrationForAllTables, androidx.sqlite.SQLiteDriver? sqliteDriver);
+    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory? sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, android.content.Intent? multiInstanceInvalidationServiceIntent, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom, String? copyFromAssetPath, java.io.File? copyFromFile, java.util.concurrent.Callable<java.io.InputStream>? copyFromInputStream, androidx.room.RoomDatabase.PrepackagedDatabaseCallback? prepackagedDatabaseCallback, java.util.List<?> typeConverters, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs, boolean allowDestructiveMigrationForAllTables, androidx.sqlite.SQLiteDriver? sqliteDriver, kotlin.coroutines.CoroutineContext? queryCoroutineContext);
     ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, boolean multiInstanceInvalidation, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom);
     ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, boolean multiInstanceInvalidation, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom, String? copyFromAssetPath, java.io.File? copyFromFile);
     ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, boolean multiInstanceInvalidation, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom, String? copyFromAssetPath, java.io.File? copyFromFile, java.util.concurrent.Callable<java.io.InputStream>? copyFromInputStream);
@@ -42,6 +42,7 @@
     field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final android.content.Intent? multiInstanceInvalidationServiceIntent;
     field public final String? name;
     field public final androidx.room.RoomDatabase.PrepackagedDatabaseCallback? prepackagedDatabaseCallback;
+    field public final kotlin.coroutines.CoroutineContext? queryCoroutineContext;
     field public final java.util.concurrent.Executor queryExecutor;
     field public final boolean requireMigration;
     field public final androidx.sqlite.SQLiteDriver? sqliteDriver;
@@ -134,10 +135,10 @@
   }
 
   public class InvalidationTracker {
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase database, java.lang.String... tableNames);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase database, java.lang.String... tableNames);
     ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase database, java.util.Map<java.lang.String,java.lang.String> shadowTablesMap, java.util.Map<java.lang.String,java.util.Set<java.lang.String>> viewTables, java.lang.String... tableNames);
     method @WorkerThread public void addObserver(androidx.room.InvalidationTracker.Observer observer);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void addWeakObserver(androidx.room.InvalidationTracker.Observer observer);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @WorkerThread public void addWeakObserver(androidx.room.InvalidationTracker.Observer observer);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T> createLiveData(String[] tableNames, boolean inTransaction, java.util.concurrent.Callable<T?> computeFunction);
     method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T> createLiveData(String[] tableNames, java.util.concurrent.Callable<T?> computeFunction);
     method public void refreshVersionsAsync();
@@ -243,6 +244,7 @@
     method public androidx.room.RoomDatabase.Builder<T> setJournalMode(androidx.room.RoomDatabase.JournalMode journalMode);
     method @SuppressCompatibility @androidx.room.ExperimentalRoomApi public androidx.room.RoomDatabase.Builder<T> setMultiInstanceInvalidationServiceIntent(android.content.Intent invalidationServiceIntent);
     method public androidx.room.RoomDatabase.Builder<T> setQueryCallback(androidx.room.RoomDatabase.QueryCallback queryCallback, java.util.concurrent.Executor executor);
+    method public final androidx.room.RoomDatabase.Builder<T> setQueryCoroutineContext(kotlin.coroutines.CoroutineContext context);
     method public androidx.room.RoomDatabase.Builder<T> setQueryExecutor(java.util.concurrent.Executor executor);
     method public androidx.room.RoomDatabase.Builder<T> setTransactionExecutor(java.util.concurrent.Executor executor);
   }
@@ -390,6 +392,14 @@
 
 }
 
+package androidx.room.coroutines {
+
+  public final class FlowUtil {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, kotlin.jvm.functions.Function1<? super androidx.sqlite.SQLiteConnection,? extends R> block);
+  }
+
+}
+
 package androidx.room.migration {
 
   public interface AutoMigrationSpec {
@@ -413,7 +423,7 @@
 
 package androidx.room.paging {
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class LimitOffsetDataSource<T> extends androidx.paging.PositionalDataSource<T> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class LimitOffsetDataSource<T> extends androidx.paging.PositionalDataSource<T!> {
     ctor protected LimitOffsetDataSource(androidx.room.RoomDatabase, androidx.room.RoomSQLiteQuery, boolean, boolean, java.lang.String!...);
     ctor protected LimitOffsetDataSource(androidx.room.RoomDatabase, androidx.room.RoomSQLiteQuery, boolean, java.lang.String!...);
     ctor protected LimitOffsetDataSource(androidx.room.RoomDatabase, androidx.sqlite.db.SupportSQLiteQuery, boolean, boolean, java.lang.String!...);
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index 2de218e..b2fa665 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -22,12 +22,12 @@
  * modifying its settings.
  */
 
+
 import androidx.build.LibraryType
 import androidx.build.PlatformIdentifier
 import androidx.build.Publish
-import androidx.build.SdkHelperKt
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import org.jetbrains.kotlin.konan.target.KonanTarget
+import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
     id("AndroidXPlugin")
@@ -198,14 +198,11 @@
                 test.defaultSourceSet {
                     dependsOn(nativeTest)
                 }
-                if (target.konanTarget == KonanTarget.LINUX_X64.INSTANCE) {
+                if (target.konanTarget.family == Family.LINUX) {
                     // For tests in Linux host, statically include androidx's compiled SQLite
                     // via a generated C interop definition
                     createCinteropFromArchiveConfiguration(test, configurations["sqliteSharedArchive"])
-                } else if (
-                        target.konanTarget == KonanTarget.MACOS_X64.INSTANCE ||
-                                target.konanTarget == KonanTarget.MACOS_ARM64.INSTANCE
-                ) {
+                } else if (target.konanTarget.family == Family.OSX) {
                     // For tests in Mac host, link to shared SQLite library included in MacOS
                     test.kotlinOptions.freeCompilerArgs += [
                             "-linker-options", "-lsqlite3"
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
index 2bd7623..19995f7 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
@@ -26,6 +26,7 @@
 import java.util.concurrent.Callable
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executors
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -40,13 +41,16 @@
 import org.junit.Test
 
 @SmallTest
+@OptIn(DelicateCoroutinesApi::class)
 class CoroutineRoomCancellationTest {
 
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
 
-    private val database = TestDatabase().apply {
-        init(
+    private val database = TestDatabase()
+
+    private fun initWithDispatcher(dispatcher: CoroutineDispatcher) {
+        database.init(
             DatabaseConfiguration(
                 context = InstrumentationRegistry.getInstrumentation().targetContext,
                 name = "test",
@@ -68,15 +72,15 @@
                 typeConverters = emptyList(),
                 autoMigrationSpecs = emptyList(),
                 allowDestructiveMigrationForAllTables = false,
-                sqliteDriver = null
+                sqliteDriver = null,
+                queryCoroutineContext = dispatcher
             )
         )
     }
 
-    @OptIn(DelicateCoroutinesApi::class)
     @Test
     fun testSuspend_cancellable_duringLongQuery() = runBlocking {
-        database.backingFieldMap["QueryDispatcher"] = Dispatchers.IO
+        initWithDispatcher(Dispatchers.IO)
 
         val inQueryLatch = CountDownLatch(1)
         val cancelledLatch = CountDownLatch(1)
@@ -88,6 +92,7 @@
         }
 
         val job = GlobalScope.launch(Dispatchers.IO) {
+            @Suppress("DEPRECATION")
             CoroutinesRoom.execute(
                 db = database,
                 inTransaction = false,
@@ -107,10 +112,9 @@
         assertThat(cancellationSignal.isCanceled).isTrue()
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
     @Test
     fun testSuspend_cancellable_beforeQueryStarts() = runBlocking {
-        database.backingFieldMap["QueryDispatcher"] = testDispatcher
+        initWithDispatcher(testDispatcher)
 
         val inCoroutineLatch = CountDownLatch(1)
         val cancelledLatch = CountDownLatch(1)
@@ -125,6 +129,7 @@
             // Coroutine started so now we can cancel it
             inCoroutineLatch.countDown()
 
+            @Suppress("DEPRECATION")
             CoroutinesRoom.execute(
                 db = database,
                 inTransaction = false,
@@ -142,14 +147,14 @@
         assertThat(cancellationSignal.isCanceled).isTrue()
     }
 
-    @OptIn(DelicateCoroutinesApi::class)
     @Test
     fun testSuspend_exception_in_query() = runBlocking {
-        database.backingFieldMap["QueryDispatcher"] = Dispatchers.IO
+        initWithDispatcher(Dispatchers.IO)
         val cancellationSignal = CancellationSignal()
 
         GlobalScope.launch(Dispatchers.IO) {
             try {
+                @Suppress("DEPRECATION")
                 CoroutinesRoom.execute(
                     db = database,
                     inTransaction = false,
@@ -166,14 +171,15 @@
         assertThat(cancellationSignal.isCanceled).isFalse()
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
+    @OptIn(ExperimentalCoroutinesApi::class)
     fun testSuspend_notCancelled() = runBlocking {
-        database.backingFieldMap["QueryDispatcher"] = testDispatcher
+        initWithDispatcher(testDispatcher)
 
         val cancellationSignal = CancellationSignal()
 
         val job = testScope.launch {
+            @Suppress("DEPRECATION")
             CoroutinesRoom.execute(
                 db = database,
                 inTransaction = false,
@@ -213,7 +219,9 @@
         }
     }
 
-    private class TestInvalidationTracker(db: RoomDatabase) : InvalidationTracker(db, "") {
+    private class TestInvalidationTracker(
+        db: RoomDatabase
+    ) : InvalidationTracker(db, emptyMap(), emptyMap(), "") {
         val observers = mutableListOf<Observer>()
 
         override fun addObserver(observer: Observer) {
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingDatabaseTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingDatabaseTest.kt
new file mode 100644
index 0000000..5b7570b
--- /dev/null
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingDatabaseTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.support
+
+import android.content.Context
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule
+import androidx.kruth.assertThat
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.ExperimentalRoomApi
+import androidx.room.Insert
+import androidx.room.InvalidationTracker
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.MediumTest
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+@OptIn(ExperimentalRoomApi::class)
+class AutoClosingDatabaseTest {
+    @get:Rule
+    val executorRule = CountingTaskExecutorRule()
+
+    private lateinit var db: TestDatabase
+    private lateinit var userDao: TestUserDao
+
+    @Before
+    fun createDb() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        context.deleteDatabase("testDb")
+        db = Room.databaseBuilder(context, TestDatabase::class.java, "testDb")
+            .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
+            .build()
+        userDao = db.getUserDao()
+    }
+
+    @After
+    fun cleanUp() {
+        executorRule.drainTasks(1, TimeUnit.SECONDS)
+        assertThat(executorRule.isIdle).isTrue()
+        db.close()
+    }
+
+    @Test
+    fun invalidationObserver_notifiedByTableName() = runTest {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        context.deleteDatabase("test.db")
+
+        val db: TestDatabase =
+            Room.databaseBuilder(context, TestDatabase::class.java, "test.db")
+                .setAutoCloseTimeout(0, TimeUnit.MILLISECONDS)
+                .build()
+
+        val invalidationCount = AtomicInteger(0)
+
+        db.invalidationTracker.addObserver(
+            object : InvalidationTracker.Observer("user") {
+                override fun onInvalidated(tables: Set<String>) {
+                    invalidationCount.getAndIncrement()
+                }
+            }
+        )
+
+        db.getUserDao().insert(TestUser(1, "bob"))
+
+        executorRule.drainTasks(1, TimeUnit.SECONDS)
+        assertThat(invalidationCount.get()).isEqualTo(1)
+
+        delay(100) // Let db auto close
+
+        db.invalidationTracker.notifyObserversByTableNames("user")
+
+        executorRule.drainTasks(1, TimeUnit.SECONDS)
+        assertThat(invalidationCount.get()).isEqualTo(2)
+
+        db.close()
+    }
+
+    @Database(entities = [TestUser::class], version = 1, exportSchema = false)
+    abstract class TestDatabase : RoomDatabase() {
+        abstract fun getUserDao(): TestUserDao
+    }
+
+    @Dao
+    interface TestUserDao {
+        @Insert
+        fun insert(user: TestUser)
+
+        @Query("SELECT * FROM user WHERE id = :id")
+        fun get(id: Long): TestUser
+    }
+
+    @Entity(tableName = "user")
+    data class TestUser(@PrimaryKey val id: Long, val data: String)
+}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/CoroutinesRoom.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/CoroutinesRoom.android.kt
index 408fe77..dfd95da 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/CoroutinesRoom.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/CoroutinesRoom.android.kt
@@ -18,19 +18,12 @@
 
 import android.os.CancellationSignal
 import androidx.annotation.RestrictTo
+import androidx.room.coroutines.createFlow as createFlowCommon
+import androidx.room.util.getCoroutineContext
 import java.util.concurrent.Callable
-import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emitAll
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
@@ -55,17 +48,14 @@
                 return callable.call()
             }
 
-            // Use the transaction dispatcher if we are on a transaction coroutine, otherwise
-            // use the database dispatchers.
-            val context = coroutineContext[TransactionElement]?.transactionDispatcher
-                ?: if (inTransaction) db.transactionDispatcher else db.getQueryDispatcher()
+            val context = db.getCoroutineContext(inTransaction)
             return withContext(context) {
                 callable.call()
             }
         }
 
-        @OptIn(DelicateCoroutinesApi::class)
         @JvmStatic
+        @Deprecated("No longer called by generated implementation")
         public suspend fun <R> execute(
             db: RoomDatabase,
             inTransaction: Boolean,
@@ -76,12 +66,9 @@
                 return callable.call()
             }
 
-            // Use the transaction dispatcher if we are on a transaction coroutine, otherwise
-            // use the database dispatchers.
-            val context = coroutineContext[TransactionElement]?.transactionDispatcher
-                ?: if (inTransaction) db.transactionDispatcher else db.getQueryDispatcher()
+            val context = db.getCoroutineContext(inTransaction)
             return suspendCancellableCoroutine<R> { continuation ->
-                val job = GlobalScope.launch(context) {
+                val job = db.getCoroutineScope().launch(context) {
                     try {
                         val result = callable.call()
                         continuation.resume(result)
@@ -97,60 +84,13 @@
         }
 
         @JvmStatic
+        @Deprecated("No longer called by generated implementation")
         public fun <R> createFlow(
             db: RoomDatabase,
             inTransaction: Boolean,
             tableNames: Array<String>,
             callable: Callable<R>
-        ): Flow<@JvmSuppressWildcards R> = flow {
-            coroutineScope {
-                // Observer channel receives signals from the invalidation tracker to emit queries.
-                val observerChannel = Channel<Unit>(Channel.CONFLATED)
-                val observer = object : InvalidationTracker.Observer(tableNames) {
-                    override fun onInvalidated(tables: Set<String>) {
-                        observerChannel.trySend(Unit)
-                    }
-                }
-                observerChannel.trySend(Unit) // Initial signal to perform first query.
-                val queryContext = coroutineContext[TransactionElement]?.transactionDispatcher
-                    ?: if (inTransaction) db.transactionDispatcher else db.getQueryDispatcher()
-                val resultChannel = Channel<R>()
-                launch(queryContext) {
-                    db.invalidationTracker.addObserver(observer)
-                    try {
-                        // Iterate until cancelled, transforming observer signals to query results
-                        // to be emitted to the flow.
-                        for (signal in observerChannel) {
-                            val result = callable.call()
-                            resultChannel.send(result)
-                        }
-                    } finally {
-                        db.invalidationTracker.removeObserver(observer)
-                    }
-                }
-
-                emitAll(resultChannel)
-            }
-        }
+        ): Flow<@JvmSuppressWildcards R> =
+            createFlowCommon(db, inTransaction, tableNames) { callable.call() }
     }
 }
-
-/**
- * Gets the query coroutine dispatcher.
- *
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun RoomDatabase.getQueryDispatcher(): CoroutineDispatcher {
-    return backingFieldMap.getOrPut("QueryDispatcher") {
-        queryExecutor.asCoroutineDispatcher()
-    } as CoroutineDispatcher
-}
-
-/**
- * Gets the transaction coroutine dispatcher.
- *
- */
-internal val RoomDatabase.transactionDispatcher: CoroutineDispatcher
-    get() = backingFieldMap.getOrPut("TransactionDispatcher") {
-        transactionExecutor.asCoroutineDispatcher()
-    } as CoroutineDispatcher
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/DatabaseConfiguration.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/DatabaseConfiguration.android.kt
index 5e0d283..1307781 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/DatabaseConfiguration.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/DatabaseConfiguration.android.kt
@@ -27,6 +27,7 @@
 import java.io.InputStream
 import java.util.concurrent.Callable
 import java.util.concurrent.Executor
+import kotlin.coroutines.CoroutineContext
 
 /**
  * Configuration class for a [RoomDatabase].
@@ -126,7 +127,10 @@
     val allowDestructiveMigrationForAllTables: Boolean,
 
     @JvmField
-    actual val sqliteDriver: SQLiteDriver?
+    actual val sqliteDriver: SQLiteDriver?,
+
+    @JvmField
+    actual val queryCoroutineContext: CoroutineContext?,
 ) {
     /**
      * If true, table invalidation in an instance of [RoomDatabase] is broadcast and
@@ -188,6 +192,7 @@
         autoMigrationSpecs = emptyList(),
         allowDestructiveMigrationForAllTables = false,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     /**
@@ -252,6 +257,7 @@
         autoMigrationSpecs = emptyList(),
         allowDestructiveMigrationForAllTables = false,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     /**
@@ -320,6 +326,7 @@
         autoMigrationSpecs = emptyList(),
         allowDestructiveMigrationForAllTables = false,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     /**
@@ -391,6 +398,7 @@
         autoMigrationSpecs = emptyList(),
         allowDestructiveMigrationForAllTables = false,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     /**
@@ -465,6 +473,7 @@
         autoMigrationSpecs = emptyList(),
         allowDestructiveMigrationForAllTables = false,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     /**
@@ -541,6 +550,7 @@
         autoMigrationSpecs = emptyList(),
         allowDestructiveMigrationForAllTables = false,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     /**
@@ -619,6 +629,7 @@
         autoMigrationSpecs = autoMigrationSpecs,
         allowDestructiveMigrationForAllTables = false,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     /**
@@ -694,6 +705,7 @@
         autoMigrationSpecs = autoMigrationSpecs,
         allowDestructiveMigrationForAllTables = false,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@@ -741,6 +753,7 @@
         autoMigrationSpecs = autoMigrationSpecs,
         allowDestructiveMigrationForAllTables = allowDestructiveMigrationForAllTables,
         sqliteDriver = null,
+        queryCoroutineContext = null
     )
 
     /**
@@ -770,7 +783,8 @@
         return isMigrationRequiredExt(fromVersion, toVersion)
     }
 
-    internal fun copy(
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    fun copy(
         context: Context = this.context,
         name: String? = this.name,
         sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory? = this.sqliteOpenHelperFactory,
@@ -793,7 +807,8 @@
         typeConverters: List<Any> = this.typeConverters,
         autoMigrationSpecs: List<AutoMigrationSpec> = this.autoMigrationSpecs,
         allowDestructiveMigrationForAllTables: Boolean = this.allowDestructiveMigrationForAllTables,
-        sqliteDriver: SQLiteDriver? = this.sqliteDriver
+        sqliteDriver: SQLiteDriver? = this.sqliteDriver,
+        queryCoroutineContext: CoroutineContext? = this.queryCoroutineContext
     ) = DatabaseConfiguration(
         context,
         name,
@@ -816,5 +831,6 @@
         autoMigrationSpecs,
         allowDestructiveMigrationForAllTables,
         sqliteDriver,
+        queryCoroutineContext
     )
 }
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 39464f9..f37b490 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
@@ -17,105 +17,65 @@
 
 import android.content.Context
 import android.content.Intent
-import android.database.sqlite.SQLiteException
-import android.util.Log
-import androidx.annotation.GuardedBy
 import androidx.annotation.RestrictTo
-import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
-import androidx.arch.core.internal.SafeIterableMap
 import androidx.lifecycle.LiveData
-import androidx.room.Room.LOG_TAG
-import androidx.room.driver.SupportSQLiteConnection
+import androidx.room.InvalidationTracker.Observer
 import androidx.room.support.AutoCloser
-import androidx.room.util.useCursor
 import androidx.sqlite.SQLiteConnection
-import androidx.sqlite.db.SimpleSQLiteQuery
-import androidx.sqlite.db.SupportSQLiteDatabase
-import androidx.sqlite.db.SupportSQLiteStatement
 import java.lang.ref.WeakReference
 import java.util.concurrent.Callable
-import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 
 /**
- * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * The invalidation tracker keeps track of tables modified by queries and notifies its subscribed
  * [Observer]s about such modifications.
+ *
+ * [Observer]s contain one or more tables and are added to the tracker via [subscribe]. Once
+ * an observer is subscribed, if a database operation changes one of the tables the observer is
+ * subscribed to, then such table is considered 'invalidated' and [Observer.onInvalidated] will
+ * be invoked on the observer. If an observer is no longer interested in tracking modifications
+ * it can be removed via [unsubscribe].
  */
-// Some details on how the InvalidationTracker works:
-// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
-// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
-// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
-// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
-// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
-// tables.
-// * Each update (write operation) on one of the observed tables triggers an update into the
-// memory table table, flipping the invalidated flag ON.
-// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
-// It works as an Observer, and notifies other instances of table invalidation.
 actual open class InvalidationTracker
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 actual constructor(
     internal val database: RoomDatabase,
     private val shadowTablesMap: Map<String, String>,
     private val viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
-    vararg tableNames: String
+    internal vararg val tableNames: String
 ) {
-    internal val tableIdLookup: Map<String, Int>
-    internal val tablesNames: Array<out String>
+    private val implementation =
+        TriggerBasedInvalidationTracker(database, shadowTablesMap, viewTables, tableNames)
 
     private var autoCloser: AutoCloser? = null
 
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
-    @field:RestrictTo(RestrictTo.Scope.LIBRARY)
-    val pendingRefresh = AtomicBoolean(false)
+    private val onRefreshScheduled: () -> Unit = {
+        // refreshVersionsAsync() is called with the ref count incremented from
+        // RoomDatabase, so the db can't be closed here, but we need to be sure that our
+        // db isn't closed until refresh is completed. This increment call must be
+        // matched with a corresponding call in refreshRunnable.
+        autoCloser?.incrementCountAndEnsureDbIsOpen()
+    }
 
-    @Volatile
-    private var initialized = false
-
-    @Volatile
-    internal var cleanupStatement: SupportSQLiteStatement? = null
-
-    private val observedTableTracker: ObservedTableTracker = ObservedTableTracker(tableNames.size)
+    private val onRefreshCompleted: () -> Unit = {
+        autoCloser?.decrementCountAndScheduleClose()
+    }
 
     private val invalidationLiveDataContainer: InvalidationLiveDataContainer =
         InvalidationLiveDataContainer(database)
 
-    @GuardedBy("observerMap")
-    internal val observerMap = SafeIterableMap<Observer, ObserverWrapper>()
-
-    private var multiInstanceInvalidationClient: MultiInstanceInvalidationClient? = null
-
-    private val syncTriggersLock = Any()
-
-    private val trackerLock = Any()
-
     /** The initialization state for restarting invalidation after auto-close. */
     private var multiInstanceClientInitState: MultiInstanceClientInitState? = null
 
-    init {
-        tableIdLookup = mutableMapOf()
-        tablesNames = Array(tableNames.size) { id ->
-            val tableName = tableNames[id].lowercase()
-            tableIdLookup[tableName] = id
-            val shadowTableName = shadowTablesMap[tableNames[id]]?.lowercase()
-            shadowTableName ?: tableName
-        }
+    /** The multi instance invalidation client. */
+    private var multiInstanceInvalidationClient: MultiInstanceInvalidationClient? = null
 
-        // Adjust table id lookup for those tables whose shadow table is another already mapped
-        // table (e.g. external content fts tables).
-        shadowTablesMap.forEach { entry ->
-            val shadowTableName = entry.value.lowercase()
-            if (tableIdLookup.containsKey(shadowTableName)) {
-                val tableName = entry.key.lowercase()
-                tableIdLookup[tableName] = tableIdLookup.getValue(shadowTableName)
-            }
-        }
-    }
+    private val trackerLock = Any()
 
-    /**
-     * Used by the generated code.
-     *
-     */
+    @Deprecated("No longer called by generated implementation")
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     constructor(database: RoomDatabase, vararg tableNames: String) :
         this(
@@ -125,6 +85,15 @@
             tableNames = tableNames
         )
 
+    init {
+        // TODO(b/316944352): Figure out auto-close with driver APIs
+        // Setup a callback to disallow invalidation refresh when underlying compat database
+        // is closed. This is done to support auto-close feature.
+        implementation.onAllowRefresh = {
+            !database.inCompatibilityMode() || database.isOpenInternal
+        }
+    }
+
     /**
      * Sets the auto closer for this invalidation tracker so that the invalidation tracker can
      * ensure that the database is not closed if there are pending invalidations that haven't yet
@@ -146,51 +115,54 @@
      * Internal method to initialize table tracking.
      */
     internal actual fun internalInit(connection: SQLiteConnection) {
-        if (connection is SupportSQLiteConnection) {
-            @Suppress("DEPRECATION")
-            internalInit(connection.db)
-        } else {
-            Log.e(LOG_TAG, "Invalidation tracker is disabled due to lack of driver " +
-                "support. - b/309990302")
+        implementation.configureConnection(connection)
+        synchronized(trackerLock) {
+            if (multiInstanceInvalidationClient == null && multiInstanceClientInitState != null) {
+                // Start multi-instance invalidation, based in info from the saved initState.
+                startMultiInstanceInvalidation()
+            }
         }
     }
 
     /**
-     * Internal method to initialize table tracking.
+     * Synchronize subscribed observers with their tables.
+     *
+     * This function should be called before any write operation is performed on the database
+     * so that a tracking link is created between observers and its interest tables.
+     *
+     * @see refreshAsync
      */
-    @Deprecated("No longer called by generated code")
-    internal fun internalInit(database: SupportSQLiteDatabase) {
-        synchronized(trackerLock) {
-            if (initialized) {
-                Log.e(LOG_TAG, "Invalidation tracker is initialized twice :/.")
-                return
-            }
-
-            multiInstanceClientInitState?.let {
-                // Start multi-instance invalidation, based in info from the saved initState.
-                startMultiInstanceInvalidation()
-            }
-
-            // These actions are not in a transaction because temp_store is not allowed to be
-            // performed on a transaction, and recursive_triggers is not affected by transactions.
-            database.execSQL("PRAGMA temp_store = MEMORY;")
-            database.execSQL("PRAGMA recursive_triggers='ON';")
-            database.execSQL(CREATE_TRACKING_TABLE_SQL)
-            syncTriggers(database)
-            cleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL)
-            initialized = true
+    internal actual suspend fun sync() {
+        if (database.inCompatibilityMode() && !database.isOpenInternal) {
+            return
         }
+        implementation.syncTriggers()
+    }
+
+    // TODO(b/309990302): Needed for compatibility with internalBeginTransaction(), not great.
+    internal fun syncBlocking(): Unit = runBlocking { sync() }
+
+    /**
+     * Refresh subscribed observers asynchronously, invoking [Observer.onInvalidated] on those whose
+     * tables have been invalidated.
+     *
+     * This function should be called after any write operation is performed on the database,
+     * such that tracked tables and its associated observers are notified if invalidated.
+     *
+     * @see sync
+     */
+    internal actual fun refreshAsync() {
+        implementation.refreshInvalidationAsync(onRefreshScheduled, onRefreshCompleted)
     }
 
     private fun onAutoCloseCallback() {
         synchronized(trackerLock) {
-            val isObserverMapEmpty = observerMap.filterNot { it.key.isRemote }.isEmpty()
+            val isObserverMapEmpty =
+                implementation.getAllObservers().filterNot { it.isRemote }.isEmpty()
             if (multiInstanceInvalidationClient != null && isObserverMapEmpty) {
                 stopMultiInstanceInvalidation()
             }
-            initialized = false
-            observedTableTracker.resetTriggerState()
-            cleanupStatement?.close()
+            implementation.resetSync()
         }
     }
 
@@ -210,42 +182,19 @@
         multiInstanceInvalidationClient = null
     }
 
-    private fun stopTrackingTable(db: SupportSQLiteDatabase, tableId: Int) {
-        val tableName = tablesNames[tableId]
-        for (trigger in TRIGGERS) {
-            val sql = buildString {
-                append("DROP TRIGGER IF EXISTS ")
-                append(getTriggerName(tableName, trigger))
-            }
-            db.execSQL(sql)
-        }
-    }
-
-    private fun startTrackingTable(db: SupportSQLiteDatabase, tableId: Int) {
-        db.execSQL(
-            "INSERT OR IGNORE INTO $UPDATE_TABLE_NAME VALUES($tableId, 0)"
-        )
-        val tableName = tablesNames[tableId]
-        for (trigger in TRIGGERS) {
-            val sql = buildString {
-                append("CREATE TEMP TRIGGER IF NOT EXISTS ")
-                append(getTriggerName(tableName, trigger))
-                append(" AFTER ")
-                append(trigger)
-                append(" ON `")
-                append(tableName)
-                append("` BEGIN UPDATE ")
-                append(UPDATE_TABLE_NAME)
-                append(" SET ").append(INVALIDATED_COLUMN_NAME)
-                append(" = 1")
-                append(" WHERE ").append(TABLE_ID_COLUMN_NAME)
-                append(" = ").append(tableId)
-                append(" AND ").append(INVALIDATED_COLUMN_NAME)
-                append(" = 0")
-                append("; END")
-            }
-            db.execSQL(sql)
-        }
+    /**
+     * Subscribes the given [observer] with the tracker such that it is notified if any table it
+     * is interested on changes.
+     *
+     * If the observer is already subscribed, then this function does nothing.
+     *
+     * @param observer The observer that will listen for database changes.
+     * @throws IllegalArgumentException if one of the tables in the observer does not exist.
+     */
+    // TODO(b/329315924): Replace with Flow based API
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun subscribe(observer: Observer) {
+        implementation.addObserver(observer)
     }
 
     /**
@@ -266,53 +215,8 @@
      * @param observer The observer which listens the database for changes.
      */
     @WorkerThread
-    open fun addObserver(observer: Observer) {
-        val tableNames = resolveViews(observer.tables)
-        val tableIds = tableNames.map { tableName ->
-            tableIdLookup[tableName.lowercase()]
-                ?: throw IllegalArgumentException("There is no table with name $tableName")
-        }.toIntArray()
-
-        val wrapper = ObserverWrapper(
-            observer = observer,
-            tableIds = tableIds,
-            tableNames = tableNames
-        )
-
-        val currentObserver = synchronized(observerMap) {
-            observerMap.putIfAbsent(observer, wrapper)
-        }
-        if (currentObserver == null && observedTableTracker.onAdded(*tableIds)) {
-            syncTriggers()
-        }
-    }
-
-    private fun validateAndResolveTableNames(tableNames: Array<out String>): Array<out String> {
-        val resolved = resolveViews(tableNames)
-        resolved.forEach { tableName ->
-            require(tableIdLookup.containsKey(tableName.lowercase())) {
-                "There is no table with name $tableName"
-            }
-        }
-        return resolved
-    }
-
-    /**
-     * Resolves the list of tables and views into a list of unique tables that are underlying them.
-     *
-     * @param names The names of tables or views.
-     * @return The names of the underlying tables.
-     */
-    private fun resolveViews(names: Array<out String>): Array<out String> {
-        return buildSet {
-            names.forEach { name ->
-                if (viewTables.containsKey(name.lowercase())) {
-                    addAll(viewTables[name.lowercase()]!!)
-                } else {
-                    add(name)
-                }
-            }
-        }.toTypedArray()
+    open fun addObserver(observer: Observer): Unit = runBlocking {
+        implementation.addObserver(observer)
     }
 
     /**
@@ -323,9 +227,23 @@
      *
      * @param observer The observer to which InvalidationTracker will keep a weak reference.
      */
+    @WorkerThread
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     open fun addWeakObserver(observer: Observer) {
-        addObserver(WeakObserver(this, observer))
+        addObserver(WeakObserver(this, database.getCoroutineScope(), observer))
+    }
+
+    /**
+     * Unsubscribes the given [observer] from the tracker.
+     *
+     * If the observer was never subscribed in the first place, then this function does nothing.
+     *
+     * @param observer The observer to remove.
+     */
+    // TODO(b/329315924): Replace with Flow based API
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun unsubscribe(observer: Observer) {
+        implementation.removeObserver(observer)
     }
 
     /**
@@ -337,140 +255,28 @@
      * @param observer The observer to remove.
      */
     @WorkerThread
-    open fun removeObserver(observer: Observer) {
-        val wrapper = synchronized(observerMap) {
-            observerMap.remove(observer)
-        }
-        if (wrapper != null && observedTableTracker.onRemoved(tableIds = wrapper.tableIds)) {
-            syncTriggers()
-        }
-    }
-
-    internal fun ensureInitialization(): Boolean {
-        if (!database.isOpenInternal) {
-            return false
-        }
-        if (!initialized) {
-            // trigger initialization
-            database.openHelper.writableDatabase
-        }
-        if (!initialized) {
-            Log.e(LOG_TAG, "database is not initialized even though it is open")
-            return false
-        }
-        return true
-    }
-
-    @VisibleForTesting
-    @JvmField
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    val refreshRunnable: Runnable = object : Runnable {
-        override fun run() {
-            val closeLock = database.getCloseLock()
-            closeLock.lock()
-            val invalidatedTableIds: Set<Int> =
-                try {
-                    if (!ensureInitialization()) {
-                        return
-                    }
-                    if (!pendingRefresh.compareAndSet(true, false)) {
-                        // no pending refresh
-                        return
-                    }
-                    if (database.inTransaction()) {
-                        // current thread is in a transaction. when it ends, it will invoke
-                        // refreshRunnable again. pendingRefresh is left as false on purpose
-                        // so that the last transaction can flip it on again.
-                        return
-                    }
-
-                    // This transaction has to be on the underlying DB rather than the RoomDatabase
-                    // in order to avoid a recursive loop after endTransaction.
-                    val db = database.openHelper.writableDatabase
-                    db.beginTransactionNonExclusive()
-                    val invalidatedTableIds: Set<Int>
-                    try {
-                        invalidatedTableIds = checkUpdatedTable()
-                        db.setTransactionSuccessful()
-                    } finally {
-                        db.endTransaction()
-                    }
-                    invalidatedTableIds
-                } catch (ex: IllegalStateException) {
-                    // may happen if db is closed. just log.
-                    Log.e(
-                        LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
-                        ex
-                    )
-                    emptySet()
-                } catch (ex: SQLiteException) {
-                    Log.e(
-                        LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
-                        ex
-                    )
-                    emptySet()
-                } finally {
-                    closeLock.unlock()
-                    autoCloser?.decrementCountAndScheduleClose()
-                }
-
-            if (invalidatedTableIds.isNotEmpty()) {
-                synchronized(observerMap) {
-                    observerMap.forEach {
-                        it.value.notifyByTableInvalidStatus(invalidatedTableIds)
-                    }
-                }
-            }
-        }
-
-        private fun checkUpdatedTable(): Set<Int> {
-            val invalidatedTableIds = buildSet {
-                database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor ->
-                    while (cursor.moveToNext()) {
-                        add(cursor.getInt(0))
-                    }
-                }
-            }
-            if (invalidatedTableIds.isNotEmpty()) {
-                checkNotNull(cleanupStatement)
-                val statement = cleanupStatement
-                requireNotNull(statement)
-                statement.executeUpdateDelete()
-            }
-            return invalidatedTableIds
-        }
+    open fun removeObserver(observer: Observer): Unit = runBlocking {
+        implementation.removeObserver(observer)
     }
 
     /**
      * Enqueues a task to refresh the list of updated tables.
      *
      * This method is automatically called when [RoomDatabase.endTransaction] is called but
-     * if you have another connection to the database or directly use [ ], you may need to call this
-     * manually.
+     * if you have another connection to the database or directly use
+     * [androidx.sqlite.db.SupportSQLiteDatabase], you may need to call this manually.
      */
     open fun refreshVersionsAsync() {
-        // TODO we should consider doing this sync instead of async.
-        if (pendingRefresh.compareAndSet(false, true)) {
-            // refreshVersionsAsync is called with the ref count incremented from
-            // RoomDatabase, so the db can't be closed here, but we need to be sure that our
-            // db isn't closed until refresh is completed. This increment call must be
-            // matched with a corresponding call in refreshRunnable.
-            autoCloser?.incrementCountAndEnsureDbIsOpen()
-            database.queryExecutor.execute(refreshRunnable)
-        }
+        implementation.refreshInvalidationAsync(onRefreshScheduled, onRefreshCompleted)
     }
 
     /**
      * Check versions for tables, and run observers synchronously if tables have been updated.
-     *
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     @WorkerThread
-    open fun refreshVersionsSync() {
-        // This increment call must be matched with a corresponding call in refreshRunnable.
-        autoCloser?.incrementCountAndEnsureDbIsOpen()
-        syncTriggers()
-        refreshRunnable.run()
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    open fun refreshVersionsSync(): Unit = runBlocking {
+        implementation.refreshInvalidation(onRefreshScheduled, onRefreshCompleted)
     }
 
     /**
@@ -482,69 +288,8 @@
      * @param tables The invalidated tables.
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
-    fun notifyObserversByTableNames(vararg tables: String) {
-        synchronized(observerMap) {
-            observerMap.forEach { (observer, wrapper) ->
-                if (!observer.isRemote) {
-                    wrapper.notifyByTableNames(tables)
-                }
-            }
-        }
-    }
-
-    internal fun syncTriggers(database: SupportSQLiteDatabase) {
-        if (database.inTransaction()) {
-            // we won't run this inside another transaction.
-            return
-        }
-        try {
-            val closeLock = this.database.getCloseLock()
-            closeLock.lock()
-            try {
-                // Serialize adding and removing table trackers, this is specifically important
-                // to avoid missing invalidation before a transaction starts but there are
-                // pending (possibly concurrent) observer changes.
-                synchronized(syncTriggersLock) {
-                    val tablesToSync = observedTableTracker.getTablesToSync() ?: return
-                    beginTransactionInternal(database)
-                    try {
-                        tablesToSync.forEachIndexed { tableId, syncState ->
-                            when (syncState) {
-                                ObservedTableTracker.ADD ->
-                                    startTrackingTable(database, tableId)
-                                ObservedTableTracker.REMOVE ->
-                                    stopTrackingTable(database, tableId)
-                            }
-                        }
-                        database.setTransactionSuccessful()
-                    } finally {
-                        database.endTransaction()
-                    }
-                }
-            } finally {
-                closeLock.unlock()
-            }
-        } catch (ex: IllegalStateException) {
-            // may happen if db is closed. just log.
-            Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex)
-        } catch (ex: SQLiteException) {
-            Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex)
-        }
-    }
-
-    /**
-     * Called by RoomDatabase before each beginTransaction call.
-     *
-     * It is important that pending trigger changes are applied to the database before any query
-     * runs. Otherwise, we may miss some changes.
-     *
-     * This api should eventually be public.
-     */
-    internal fun syncTriggers() {
-        if (!database.isOpenInternal) {
-            return
-        }
-        syncTriggers(database.openHelper.writableDatabase)
+    internal fun notifyObserversByTableNames(vararg tables: String) {
+        implementation.notifyInvalidatedTableNames(setOf(*tables)) { !it.isRemote }
     }
 
     /**
@@ -560,7 +305,10 @@
      * invalidates.
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    @Deprecated("Use [createLiveData(String[], boolean, Callable)]")
+    @Deprecated(
+        message = "Replaced with overload that takes 'inTransaction 'parameter.",
+        replaceWith = ReplaceWith("createLiveData(tableNames, false, computeFunction")
+    )
     open fun <T> createLiveData(
         tableNames: Array<out String>,
         computeFunction: Callable<T?>
@@ -588,9 +336,10 @@
         inTransaction: Boolean,
         computeFunction: Callable<T?>
     ): LiveData<T> {
-        return invalidationLiveDataContainer.create(
-            validateAndResolveTableNames(tableNames), inTransaction, computeFunction
-        )
+        // Validate names early to fail fast as actual observer subscription is done once LiveData
+        // is observed.
+        implementation.validateTableNames(tableNames)
+        return invalidationLiveDataContainer.create(tableNames, inTransaction, computeFunction)
     }
 
     internal fun initMultiInstanceInvalidation(
@@ -605,278 +354,78 @@
         )
     }
 
-    internal fun stop() {
+    /**
+     * Stops invalidation tracker operations.
+     */
+    internal actual fun stop() {
         stopMultiInstanceInvalidation()
     }
 
     /**
-     * Wraps an observer and keeps the table information.
+     * An observer that can listen for changes in the database by subscribing to an
+     * [InvalidationTracker].
      *
-     * Internally table ids are used which may change from database to database so the table
-     * related information is kept here rather than in the Observer.
+     * @param tables The names of the tables this observer is interested in getting notified if
+     * they are modified.
      */
-    internal class ObserverWrapper(
-        internal val observer: Observer,
-        internal val tableIds: IntArray,
-        private val tableNames: Array<out String>
+    actual abstract class Observer actual constructor(
+        internal actual val tables: Array<out String>
     ) {
-        private val singleTableSet = if (tableNames.isNotEmpty()) {
-            setOf(tableNames[0])
-        } else {
-            emptySet()
-        }
-
-        init {
-            check(tableIds.size == tableNames.size)
-        }
-
         /**
-         * Notifies the underlying [.mObserver] if any of the observed tables are invalidated
-         * based on the given invalid status set.
-         *
-         * @param invalidatedTablesIds The table ids of the tables that are invalidated.
-         */
-        internal fun notifyByTableInvalidStatus(invalidatedTablesIds: Set<Int?>) {
-            val invalidatedTables = when (tableIds.size) {
-                0 -> emptySet()
-                1 -> if (invalidatedTablesIds.contains(tableIds[0])) {
-                    singleTableSet // Optimization for a single-table observer
-                } else {
-                    emptySet()
-                }
-                else -> buildSet {
-                    tableIds.forEachIndexed { idx, tableId ->
-                        if (invalidatedTablesIds.contains(tableId)) {
-                            add(tableNames[idx])
-                        }
-                    }
-                }
-            }
-
-            if (invalidatedTables.isNotEmpty()) {
-                observer.onInvalidated(invalidatedTables)
-            }
-        }
-
-        /**
-         * Notifies the underlying [.mObserver] if it observes any of the specified
-         * `tables`.
-         *
-         * @param tables The invalidated table names.
-         */
-        internal fun notifyByTableNames(tables: Array<out String>) {
-            val invalidatedTables = when (tableNames.size) {
-                0 -> emptySet()
-                1 -> if (tables.any { it.equals(tableNames[0], ignoreCase = true) }) {
-                    singleTableSet // Optimization for a single-table observer
-                } else {
-                    emptySet()
-                }
-                else -> buildSet {
-                    tables.forEach { table ->
-                        tableNames.forEach ourTablesLoop@{ ourTable ->
-                            if (ourTable.equals(table, ignoreCase = true)) {
-                                add(ourTable)
-                                return@ourTablesLoop
-                            }
-                        }
-                    }
-                }
-            }
-            if (invalidatedTables.isNotEmpty()) {
-                observer.onInvalidated(invalidatedTables)
-            }
-        }
-    }
-
-    /**
-     * An observer that can listen for changes in the database.
-     */
-    abstract class Observer(internal val tables: Array<out String>) {
-        /**
-         * Observes the given list of tables and views.
+         * Creates an observer for the given tables and views.
          *
          * @param firstTable The name of the table or view.
          * @param rest       More names of tables or views.
          */
-        protected constructor(firstTable: String, vararg rest: String) : this(
-            buildList {
-                addAll(rest)
-                add(firstTable)
-            }.toTypedArray()
-        )
+        protected actual constructor(
+            firstTable: String,
+            vararg rest: String
+        ) : this(arrayOf(firstTable, *rest))
 
         /**
-         * Called when one of the observed tables is invalidated in the database.
+         * Invoked when one of the observed tables is invalidated (changed).
          *
-         * @param tables A set of invalidated tables. This is useful when the observer targets
-         * multiple tables and you want to know which table is invalidated. This will
-         * be names of underlying tables when you are observing views.
+         * @param tables A set of invalidated tables. When the observer is interested in multiple
+         * tables, this set can be used to distinguish which of the observed tables were
+         * invalidated. When observing a database view the names of underlying tables will be in
+         * the set instead of the view name.
          */
-        abstract fun onInvalidated(tables: Set<String>)
+        actual abstract fun onInvalidated(tables: Set<String>)
 
         internal open val isRemote: Boolean
             get() = false
     }
 
     /**
-     * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
-     * triggers in the database.
-     *
-     * This class is thread safe
-     */
-    internal class ObservedTableTracker(tableCount: Int) {
-        // number of observers per table
-        val tableObservers = LongArray(tableCount)
-
-        // trigger state for each table at last sync
-        // this field is updated when syncAndGet is called.
-        private val triggerStates = BooleanArray(tableCount)
-
-        // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
-        private val triggerStateChanges = IntArray(tableCount)
-
-        var needsSync = false
-
-        /**
-         * @return true if # of triggers is affected.
-         */
-        fun onAdded(vararg tableIds: Int): Boolean {
-            var needTriggerSync = false
-            synchronized(this) {
-                tableIds.forEach { tableId ->
-                    val prevObserverCount = tableObservers[tableId]
-                    tableObservers[tableId] = prevObserverCount + 1
-                    if (prevObserverCount == 0L) {
-                        needsSync = true
-                        needTriggerSync = true
-                    }
-                }
-            }
-            return needTriggerSync
-        }
-
-        /**
-         * @return true if # of triggers is affected.
-         */
-        fun onRemoved(vararg tableIds: Int): Boolean {
-            var needTriggerSync = false
-            synchronized(this) {
-                tableIds.forEach { tableId ->
-                    val prevObserverCount = tableObservers[tableId]
-                    tableObservers[tableId] = prevObserverCount - 1
-                    if (prevObserverCount == 1L) {
-                        needsSync = true
-                        needTriggerSync = true
-                    }
-                }
-            }
-            return needTriggerSync
-        }
-
-        /**
-         * If we are re-opening the db we'll need to add all the triggers that we need so change
-         * the current state to false for all.
-         */
-        fun resetTriggerState() {
-            synchronized(this) {
-                triggerStates.fill(element = false)
-                needsSync = true
-            }
-        }
-
-        /**
-         * If this returns non-null, you must call onSyncCompleted.
-         *
-         * @return int[] An int array where the index for each tableId has the action for that
-         * table.
-         */
-        @VisibleForTesting
-        @JvmName("getTablesToSync")
-        fun getTablesToSync(): IntArray? {
-            synchronized(this) {
-                if (!needsSync) {
-                    return null
-                }
-                tableObservers.forEachIndexed { i, observerCount ->
-                    val newState = observerCount > 0
-                    if (newState != triggerStates[i]) {
-                        triggerStateChanges[i] = if (newState) ADD else REMOVE
-                    } else {
-                        triggerStateChanges[i] = NO_OP
-                    }
-                    triggerStates[i] = newState
-                }
-                needsSync = false
-                return triggerStateChanges.clone()
-            }
-        }
-
-        internal companion object {
-            const val NO_OP = 0 // don't change trigger state for this table
-            const val ADD = 1 // add triggers for this table
-            const val REMOVE = 2 // remove triggers for this table
-        }
-    }
-
-    /**
      * An Observer wrapper that keeps a weak reference to the given object.
      *
      * This class will automatically unsubscribe when the wrapped observer goes out of memory.
      */
-    internal class WeakObserver(
+    private class WeakObserver(
         val tracker: InvalidationTracker,
+        val coroutineScope: CoroutineScope,
         delegate: Observer
     ) : Observer(delegate.tables) {
-        val delegateRef: WeakReference<Observer> = WeakReference(delegate)
+        private val delegateRef: WeakReference<Observer> = WeakReference(delegate)
         override fun onInvalidated(tables: Set<String>) {
             val observer = delegateRef.get()
             if (observer == null) {
-                tracker.removeObserver(this)
+                coroutineScope.launch { tracker.unsubscribe(this@WeakObserver) }
             } else {
                 observer.onInvalidated(tables)
             }
         }
     }
 
-    companion object {
-        private val TRIGGERS = arrayOf("UPDATE", "DELETE", "INSERT")
-        private const val UPDATE_TABLE_NAME = "room_table_modification_log"
-        private const val TABLE_ID_COLUMN_NAME = "table_id"
-        private const val INVALIDATED_COLUMN_NAME = "invalidated"
-        private const val CREATE_TRACKING_TABLE_SQL =
-            "CREATE TEMP TABLE $UPDATE_TABLE_NAME ($TABLE_ID_COLUMN_NAME INTEGER PRIMARY KEY, " +
-                "$INVALIDATED_COLUMN_NAME INTEGER NOT NULL DEFAULT 0)"
+    /**
+     * Stores needed info to restart the invalidation after it was auto-closed.
+     */
+    private data class MultiInstanceClientInitState(
+        val context: Context,
+        val name: String,
+        val serviceIntent: Intent
+    )
 
-        @VisibleForTesting
-        internal const val RESET_UPDATED_TABLES_SQL =
-            "UPDATE $UPDATE_TABLE_NAME SET $INVALIDATED_COLUMN_NAME = 0 " +
-                "WHERE $INVALIDATED_COLUMN_NAME = 1"
-
-        @VisibleForTesting
-        internal const val SELECT_UPDATED_TABLES_SQL =
-            "SELECT * FROM $UPDATE_TABLE_NAME WHERE $INVALIDATED_COLUMN_NAME = 1;"
-
-        internal fun getTriggerName(
-            tableName: String,
-            triggerType: String
-        ) = "`room_table_modification_trigger_${tableName}_$triggerType`"
-
-        internal fun beginTransactionInternal(database: SupportSQLiteDatabase) {
-            if (database.isWriteAheadLoggingEnabled) {
-                database.beginTransactionNonExclusive()
-            } else {
-                database.beginTransaction()
-            }
-        }
-    }
+    // Kept for binary compatibility even if empty. :(
+    companion object
 }
-
-/**
- * Stores needed info to restart the invalidation after it was auto-closed.
- */
-internal data class MultiInstanceClientInitState(
-    val context: Context,
-    val name: String,
-    val serviceIntent: Intent
-)
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/MultiInstanceInvalidationClient.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/MultiInstanceInvalidationClient.android.kt
index fce2ec7..f76e86f 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/MultiInstanceInvalidationClient.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/MultiInstanceInvalidationClient.android.kt
@@ -91,8 +91,8 @@
 
     init {
         // Use all tables names for observer.
-        val tableNames: Set<String> = invalidationTracker.tableIdLookup.keys
-        observer = object : InvalidationTracker.Observer(tableNames.toTypedArray()) {
+        val tableNames = invalidationTracker.tableNames
+        observer = object : InvalidationTracker.Observer(tableNames) {
             override fun onInvalidated(tables: Set<String>) {
                 if (stopped.get()) {
                     return
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/Room.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/Room.android.kt
index 56a97d2..d6a942a 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/Room.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/Room.android.kt
@@ -26,9 +26,9 @@
     internal const val LOG_TAG = "ROOM"
 
     /**
-     * The master table where room keeps its metadata information.
+     * The master table name where Room keeps its metadata information.
      */
-    const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME
+    actual const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME
 
     /**
      * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
index 6b3c7ae..2083ced 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
@@ -35,10 +35,11 @@
 internal actual class RoomConnectionManager : BaseRoomConnectionManager {
 
     override val configuration: DatabaseConfiguration
-    override val connectionPool: ConnectionPool
     override val openDelegate: RoomOpenDelegate
     override val callbacks: List<RoomDatabase.Callback>
 
+    private val connectionPool: ConnectionPool
+
     internal val supportOpenHelper: SupportSQLiteOpenHelper?
         get() = (connectionPool as? SupportConnectionPool)?.supportDriver?.openHelper
 
@@ -49,6 +50,8 @@
         openDelegate: RoomOpenDelegate
     ) {
         this.configuration = config
+        this.openDelegate = openDelegate
+        this.callbacks = config.callbacks ?: emptyList()
         if (config.sqliteDriver == null) {
             // Compatibility mode due to no driver provided, instead a driver (SupportSQLiteDriver)
             // is created that wraps SupportSQLite* APIs. The underlying SupportSQLiteDatabase will
@@ -79,8 +82,6 @@
                 )
             }
         }
-        this.openDelegate = openDelegate
-        this.callbacks = config.callbacks ?: emptyList()
         init()
     }
 
@@ -90,6 +91,7 @@
     ) {
         this.configuration = config
         this.openDelegate = NoOpOpenDelegate()
+        this.callbacks = config.callbacks ?: emptyList()
         // Compatibility mode due to no driver provided, the SupportSQLiteDriver and
         // SupportConnectionPool are created. A Room onOpen callback is installed so that the
         // SupportSQLiteDatabase is extracted out of the RoomOpenHelper installed.
@@ -98,7 +100,6 @@
         this.connectionPool = SupportConnectionPool(
             SupportSQLiteDriver(supportOpenHelperFactory.invoke(configWithCompatibilityCallback))
         )
-        this.callbacks = config.callbacks ?: emptyList()
         init()
     }
 
@@ -198,7 +199,7 @@
     private class SupportConnectionPool(
         val supportDriver: SupportSQLiteDriver
     ) : ConnectionPool {
-        private val supportConnection by lazy(LazyThreadSafetyMode.NONE) {
+        private val supportConnection by lazy(LazyThreadSafetyMode.PUBLICATION) {
             SupportPooledConnection(supportDriver.open())
         }
 
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index f7c5d6b..4b5dbc5 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -33,6 +33,7 @@
 import androidx.annotation.WorkerThread
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.room.Room.LOG_TAG
+import androidx.room.concurrent.CloseBarrier
 import androidx.room.driver.SupportSQLiteConnection
 import androidx.room.migration.AutoMigrationSpec
 import androidx.room.migration.Migration
@@ -42,9 +43,10 @@
 import androidx.room.support.PrePackagedCopyOpenHelper
 import androidx.room.support.PrePackagedCopyOpenHelperFactory
 import androidx.room.support.QueryInterceptorOpenHelperFactory
-import androidx.room.util.contains as containsExt
+import androidx.room.util.contains as containsCommon
 import androidx.room.util.findAndInstantiateDatabaseImpl
 import androidx.room.util.findMigrationPath as findMigrationPathExt
+import androidx.room.util.getCoroutineContext
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
 import androidx.sqlite.db.SimpleSQLiteQuery
@@ -55,7 +57,6 @@
 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
 import java.io.File
 import java.io.InputStream
-import java.util.Collections
 import java.util.TreeMap
 import java.util.concurrent.Callable
 import java.util.concurrent.Executor
@@ -63,16 +64,21 @@
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicInteger
-import java.util.concurrent.locks.Lock
-import java.util.concurrent.locks.ReentrantReadWriteLock
 import kotlin.coroutines.ContinuationInterceptor
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.resume
 import kotlin.reflect.KClass
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.asContextElement
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
@@ -103,6 +109,9 @@
     )
     protected var mDatabase: SupportSQLiteDatabase? = null
 
+    private lateinit var coroutineScope: CoroutineScope
+    private lateinit var transactionContext: CoroutineContext
+
     /**
      * The Executor in use by this database for async queries.
      */
@@ -138,7 +147,18 @@
      *
      * @return The invalidation tracker for the database.
      */
-    actual open val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+    actual open val invalidationTracker: InvalidationTracker
+        get() = internalTracker
+
+    private lateinit var internalTracker: InvalidationTracker
+
+    /**
+     * A barrier that prevents the database from closing while the [InvalidationTracker] is using
+     * the database asynchronously.
+     *
+     * @return The barrier for [close].
+     */
+    internal actual val closeBarrier = CloseBarrier(::onClosed)
 
     private var allowMainThreadQueries = false
 
@@ -150,24 +170,9 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     protected var mCallbacks: List<Callback>? = null
 
-    private val readWriteLock = ReentrantReadWriteLock()
     private var autoCloser: AutoCloser? = null
 
     /**
-     * [InvalidationTracker] uses this lock to prevent the database from closing while it is
-     * querying database updates.
-     *
-     * The returned lock is reentrant and will allow multiple threads to acquire the lock
-     * simultaneously until [close] is invoked in which the lock becomes exclusive as
-     * a way to let the InvalidationTracker finish its work before closing the database.
-     *
-     * @return The lock for [close].
-     */
-    internal fun getCloseLock(): Lock {
-        return readWriteLock.readLock()
-    }
-
-    /**
      * Suspending transaction id of the current thread.
      *
      * This id is only set on threads that are used to dispatch coroutines within a suspending
@@ -176,13 +181,6 @@
     @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     val suspendingTransactionId = ThreadLocal<Int>()
 
-    /**
-     * Gets the map for storing extension properties of Kotlin type.
-     *
-     */
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    val backingFieldMap: MutableMap<String, Any> = Collections.synchronizedMap(mutableMapOf())
-
     private val typeConverters: MutableMap<KClass<*>, Any> = mutableMapOf()
 
     /**
@@ -230,8 +228,10 @@
      * @throws IllegalArgumentException if initialization fails.
      */
     @CallSuper
-    open fun init(configuration: DatabaseConfiguration) {
+    @OptIn(ExperimentalCoroutinesApi::class) // For limitedParallelism(1)
+    actual open fun init(configuration: DatabaseConfiguration) {
         connectionManager = createConnectionManager(configuration)
+        internalTracker = createInvalidationTracker()
         validateAutoMigrations(configuration)
         validateTypeConverters(configuration)
 
@@ -250,8 +250,40 @@
             invalidationTracker.setAutoCloser(it.autoCloser)
         }
 
-        internalQueryExecutor = configuration.queryExecutor
-        internalTransactionExecutor = TransactionExecutor(configuration.transactionExecutor)
+        if (configuration.queryCoroutineContext != null) {
+            // For backwards compatibility with internals not converted to Coroutines, use the
+            // provided dispatcher as executor.
+            val dispatcher =
+                configuration.queryCoroutineContext[ContinuationInterceptor] as CoroutineDispatcher
+            internalQueryExecutor = dispatcher.asExecutor()
+            internalTransactionExecutor = TransactionExecutor(internalQueryExecutor)
+            // For Room's coroutine scope, we use the provided context but add a SupervisorJob that
+            // is tied to the given Job (if any).
+            val parentJob = configuration.queryCoroutineContext[Job]
+            coroutineScope = CoroutineScope(
+                configuration.queryCoroutineContext + SupervisorJob(parentJob)
+            )
+            transactionContext = if (inCompatibilityMode()) {
+                // To prevent starvation due to primary connection blocking in SupportSQLiteDatabase
+                // a limited dispatcher is used for transactions.
+                coroutineScope.coroutineContext + dispatcher.limitedParallelism(1)
+            } else {
+                // When a SQLiteDriver is provided a suspending connection pool is used and there
+                // is no reason to limit parallelism.
+                coroutineScope.coroutineContext
+            }
+        } else {
+            internalQueryExecutor = configuration.queryExecutor
+            internalTransactionExecutor = TransactionExecutor(configuration.transactionExecutor)
+            // For Room's coroutine scope, we use the provided executor as dispatcher along with a
+            // SupervisorJob.
+            coroutineScope = CoroutineScope(
+                internalQueryExecutor.asCoroutineDispatcher() + SupervisorJob()
+            )
+            transactionContext = coroutineScope.coroutineContext +
+                internalTransactionExecutor.asCoroutineDispatcher()
+        }
+
         allowMainThreadQueries = configuration.allowMainThreadQueries
 
         // Configure multi-instance invalidation, if enabled
@@ -381,6 +413,19 @@
      */
     protected actual abstract fun createInvalidationTracker(): InvalidationTracker
 
+    internal actual fun getCoroutineScope(): CoroutineScope {
+        return coroutineScope
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    fun getQueryContext(): CoroutineContext {
+        return coroutineScope.coroutineContext
+    }
+
+    internal fun getTransactionContext(): CoroutineContext {
+        return transactionContext
+    }
+
     /**
      * Returns a Map of String -> List&lt;Class&gt; where each entry has the `key` as the DAO name
      * and `value` as the list of type converter classes that are necessary for the database to
@@ -474,8 +519,9 @@
                 if (hasForeignKeys && !supportsDeferForeignKeys) {
                     connection.execSQL("PRAGMA foreign_keys = FALSE")
                 }
-                // TODO(b/309990302): Commonize Invalidation Tracker
-                invalidationTracker.syncTriggers(openHelper.writableDatabase)
+                if (!connection.inTransaction()) {
+                    invalidationTracker.sync()
+                }
                 connection.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) {
                     if (hasForeignKeys && supportsDeferForeignKeys) {
                         execSQL("PRAGMA defer_foreign_keys = TRUE")
@@ -490,7 +536,7 @@
                 if (!connection.inTransaction()) {
                     connection.execSQL("PRAGMA wal_checkpoint(FULL)")
                     connection.execSQL("VACUUM")
-                    invalidationTracker.refreshVersionsAsync()
+                    invalidationTracker.refreshAsync()
                 }
             }
         }
@@ -520,16 +566,16 @@
      * Once a [RoomDatabase] is closed it should no longer be used.
      */
     actual open fun close() {
-        if (isOpen) {
-            val closeLock: Lock = readWriteLock.writeLock()
-            closeLock.lock()
-            try {
-                invalidationTracker.stop()
-                connectionManager.close()
-            } finally {
-                closeLock.unlock()
-            }
+        if (inCompatibilityMode() && !isOpen) {
+            return
         }
+        closeBarrier.close()
+    }
+
+    private fun onClosed() {
+        coroutineScope.cancel()
+        invalidationTracker.stop()
+        connectionManager.close()
     }
 
     /** True if the calling thread is the main thread.  */
@@ -651,7 +697,9 @@
     private fun internalBeginTransaction() {
         assertNotMainThread()
         val database = openHelper.writableDatabase
-        invalidationTracker.syncTriggers(database)
+        if (!database.inTransaction()) {
+            invalidationTracker.syncBlocking()
+        }
         if (database.isWriteAheadLoggingEnabled) {
             database.beginTransactionNonExclusive()
         } else {
@@ -902,6 +950,7 @@
         private var copyFromInputStream: Callable<InputStream>? = null
 
         private var driver: SQLiteDriver? = null
+        private var queryCoroutineContext: CoroutineContext? = null
 
         /**
          * Configures Room to create and open the database using a pre-packaged database located in
@@ -1175,17 +1224,26 @@
          * When both the query executor and transaction executor are unset, then a default
          * `Executor` will be used. The default `Executor` allocates and shares threads
          * amongst Architecture Components libraries. If the query executor is unset but a
-         * transaction executor was set [setTransactionExecutor], then the same `Executor` will be
-         * used for queries.
+         * transaction executor was set via [setTransactionExecutor], then the same `Executor` will
+         * be used for queries.
          *
          * For best performance the given `Executor` should be bounded (max number of threads
          * is limited).
          *
          * The input `Executor` cannot run tasks on the UI thread.
          *
+         * If either [setQueryCoroutineContext] has been called, then this function will throw an
+         * [IllegalArgumentException].
+         *
          * @return This builder instance.
+         * @throws IllegalArgumentException if this builder was already configured with a
+         * [CoroutineContext].
          */
         open fun setQueryExecutor(executor: Executor) = apply {
+            require(queryCoroutineContext == null) {
+                "This builder has already been configured with a CoroutineContext. A RoomDatabase" +
+                    "can only be configured with either an Executor or a CoroutineContext."
+            }
             this.queryExecutor = executor
         }
 
@@ -1207,9 +1265,18 @@
          *
          * The input `Executor` cannot run tasks on the UI thread.
          *
+         * If either [setQueryCoroutineContext] has been called, then this function will throw an
+         * [IllegalArgumentException].
+         *
          * @return This builder instance.
+         * @throws IllegalArgumentException if this builder was already configured with a
+         * [CoroutineContext].
          */
         open fun setTransactionExecutor(executor: Executor) = apply {
+            require(queryCoroutineContext == null) {
+                "This builder has already been configured with a CoroutineContext. A RoomDatabase" +
+                    "can only be configured with either an Executor or a CoroutineContext."
+            }
             this.transactionExecutor = executor
         }
 
@@ -1526,6 +1593,37 @@
         }
 
         /**
+         * Sets the [CoroutineContext] that will be used to execute all asynchronous queries and
+         * tasks, such as `Flow` emissions and [InvalidationTracker] notifications.
+         *
+         * If no [CoroutineDispatcher] is present in the [context] then this function will throw
+         * an [IllegalArgumentException]
+         *
+         * If no context is provided, then Room wil default to using the [Executor] set via
+         * [setQueryExecutor] as the context via the conversion function [asCoroutineDispatcher].
+         *
+         * If either [setQueryExecutor] or [setTransactionExecutor] has been called, then this
+         * function will throw an [IllegalArgumentException].
+         *
+         * @param context The context
+         * @return This [Builder] instance
+         * @throws IllegalArgumentException if no [CoroutineDispatcher] is found in the given
+         * [context] or if this builder was already configured with an [Executor].
+         *
+         */
+        @Suppress("MissingGetterMatchingBuilder")
+        actual fun setQueryCoroutineContext(context: CoroutineContext) = apply {
+            require(queryExecutor == null && transactionExecutor == null) {
+                "This builder has already been configured with an Executor. A RoomDatabase can" +
+                    "only be configured with either an Executor or a CoroutineContext."
+            }
+            require(context[ContinuationInterceptor] != null) {
+                "It is required that the coroutine context contain a dispatcher."
+            }
+            this.queryCoroutineContext = context
+        }
+
+        /**
          * Creates the databases and initializes it.
          *
          * By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive
@@ -1650,6 +1748,7 @@
                 autoMigrationSpecs,
                 allowDestructiveMigrationForAllTables,
                 driver,
+                queryCoroutineContext,
             )
             val db = factory?.invoke() ?: findAndInstantiateDatabaseImpl(klass.java)
             db.init(configuration)
@@ -1680,7 +1779,7 @@
          *
          * @param migrations List of available migrations.
          */
-        open fun addMigrations(migrations: List<Migration>) {
+        actual open fun addMigrations(migrations: List<Migration>) {
             migrations.forEach(::addMigration)
         }
 
@@ -1690,7 +1789,8 @@
          *
          * @param migration the migration to add.
          */
-        internal actual fun addMigration(migration: Migration) {
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        actual fun addMigration(migration: Migration) {
             val start = migration.startVersion
             val end = migration.endVersion
             val targetMap = migrations.getOrPut(start) { TreeMap<Int, Migration>() }
@@ -1732,8 +1832,8 @@
          * @param endVersion End version of the migration
          * @return True if it contains a migration with the same start-end version, false otherwise.
          */
-        fun contains(startVersion: Int, endVersion: Int): Boolean {
-            return this.containsExt(startVersion, endVersion)
+        actual fun contains(startVersion: Int, endVersion: Int): Boolean {
+            return this.containsCommon(startVersion, endVersion)
         }
 
         internal actual fun getSortedNodes(
@@ -2045,8 +2145,9 @@
             trySend(tables)
         }
     }
-    val queryContext =
-        coroutineContext[TransactionElement]?.transactionDispatcher ?: getQueryDispatcher()
+    // Use the database context, minus the Job since the ProducerScope has one already and the
+    // child coroutine should be tied to it.
+    val queryContext = getCoroutineContext(inTransaction = false).minusKey(Job)
     val job = launch(queryContext) {
         invalidationTracker.addObserver(observer)
         try {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnection.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnection.android.kt
index a80722a..470036a 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnection.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnection.android.kt
@@ -16,10 +16,12 @@
 
 package androidx.room.driver
 
+import androidx.annotation.RestrictTo
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.db.SupportSQLiteDatabase
 
-internal class SupportSQLiteConnection(
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class SupportSQLiteConnection(
     val db: SupportSQLiteDatabase
 ) : SQLiteConnection {
     override fun prepare(sql: String): SupportSQLiteStatement {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteDriver.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteDriver.android.kt
index a5801f3..e0fcabb 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteDriver.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteDriver.android.kt
@@ -16,10 +16,12 @@
 
 package androidx.room.driver
 
+import androidx.annotation.RestrictTo
 import androidx.sqlite.SQLiteDriver
 import androidx.sqlite.db.SupportSQLiteOpenHelper
 
-internal class SupportSQLiteDriver(
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class SupportSQLiteDriver(
     val openHelper: SupportSQLiteOpenHelper
 ) : SQLiteDriver {
     override fun open(): SupportSQLiteConnection {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
index b3ec4a1..13f49dd 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
@@ -18,6 +18,7 @@
 
 import android.database.Cursor
 import android.database.DatabaseUtils
+import androidx.annotation.RestrictTo
 import androidx.sqlite.SQLiteStatement
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.db.SupportSQLiteProgram
@@ -26,7 +27,8 @@
 
 private typealias SupportStatement = androidx.sqlite.db.SupportSQLiteStatement
 
-internal sealed class SupportSQLiteStatement(
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+sealed class SupportSQLiteStatement(
     protected val db: SupportSQLiteDatabase,
     protected val sql: String,
 ) : SQLiteStatement {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
index 430ec5b..52b1afc 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
@@ -24,13 +24,10 @@
 import android.os.Build
 import android.os.CancellationSignal
 import androidx.annotation.RestrictTo
-import androidx.room.PooledConnection
 import androidx.room.RoomDatabase
-import androidx.room.Transactor
+import androidx.room.TransactionElement
 import androidx.room.coroutines.RawConnectionAccessor
 import androidx.room.driver.SupportSQLiteConnection
-import androidx.room.getQueryDispatcher
-import androidx.room.withTransactionContext
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.db.SupportSQLiteQuery
@@ -38,6 +35,8 @@
 import java.io.FileInputStream
 import java.io.IOException
 import java.nio.ByteBuffer
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 
@@ -91,52 +90,37 @@
     db.internalPerform(isReadOnly = false, inTransaction = true) { block.invoke() }
 }
 
-private suspend inline fun <R> RoomDatabase.internalPerform(
-    isReadOnly: Boolean,
-    inTransaction: Boolean,
-    crossinline block: suspend (PooledConnection) -> R
-): R = useConnection(isReadOnly) { transactor ->
-    if (inTransaction) {
-        val type = if (isReadOnly) {
-            Transactor.SQLiteTransactionType.DEFERRED
-        } else {
-            Transactor.SQLiteTransactionType.IMMEDIATE
-        }
-        // TODO(b/309990302): Commonize Invalidation Tracker
-        if (inCompatibilityMode() && !isReadOnly) {
-            invalidationTracker.syncTriggers(openHelper.writableDatabase)
-        }
-        val result = transactor.withTransaction(type) { block.invoke(this) }
-        if (inCompatibilityMode() && !isReadOnly && !transactor.inTransaction()) {
-            invalidationTracker.refreshVersionsAsync()
-        }
-        result
-    } else {
-        block.invoke(transactor)
-    }
-}
-
 /**
- * Compatibility dispatcher behaviour in [androidx.room.CoroutinesRoom.execute] for driver codegen
- * utility functions. With the additional behaviour that it will use [withTransactionContext] if
- * performing a transaction.
+ * Compatibility suspend function execution with driver usage. This will maintain the dispatcher
+ * behaviour in [androidx.room.CoroutinesRoom.execute] when Room is in compatibility mode executing
+ * driver codegen utility functions.
  */
 private suspend inline fun <R> RoomDatabase.compatCoroutineExecute(
     inTransaction: Boolean,
     crossinline block: suspend () -> R
 ): R {
-    if (inCompatibilityMode()) {
-        if (isOpenInternal && inTransaction()) {
-            return block.invoke()
-        }
-        if (inTransaction) {
-            return withTransactionContext { block.invoke() }
-        } else {
-            return withContext(getQueryDispatcher()) { block.invoke() }
-        }
-    } else {
+    if (inCompatibilityMode() && isOpenInternal && inTransaction()) {
         return block.invoke()
     }
+    return withContext(getCoroutineContext(inTransaction)) { block.invoke() }
+}
+
+/**
+ * Gets the database [CoroutineContext] to perform database operation on utility functions. Prefer
+ * using this function over directly accessing [RoomDatabase.getCoroutineScope] as it has platform
+ * compatibility behaviour.
+ */
+internal actual suspend fun RoomDatabase.getCoroutineContext(
+    inTransaction: Boolean
+): CoroutineContext {
+    return if (inCompatibilityMode()) {
+        // If in compatibility mode check if we are on a transaction coroutine, if so combine
+        // it with the database context, otherwise use the database dispatchers.
+        coroutineContext[TransactionElement]?.transactionDispatcher?.let { getQueryContext() + it }
+            ?: if (inTransaction) getTransactionContext() else getQueryContext()
+    } else {
+        getCoroutineScope().coroutineContext
+    }
 }
 
 /**
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/BuilderTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/BuilderTest.kt
index 7019d49..7927ee5 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/BuilderTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/BuilderTest.kt
@@ -17,6 +17,7 @@
 
 import android.content.Context
 import androidx.kruth.assertThat
+import androidx.kruth.assertThrows
 import androidx.room.Room.databaseBuilder
 import androidx.room.Room.inMemoryDatabaseBuilder
 import androidx.room.migration.Migration
@@ -27,6 +28,8 @@
 import instantiateImpl
 import java.io.File
 import java.util.concurrent.Executor
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.Dispatchers
 import org.junit.Assert
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -83,8 +86,8 @@
             mock(), TestDatabase::class.java, "foo"
         ).setQueryExecutor(executor).build()
 
-        assertThat(db.mDatabaseConfiguration.queryExecutor).isEqualTo(executor)
-        assertThat(db.mDatabaseConfiguration.transactionExecutor).isEqualTo(executor)
+        assertThat(db.databaseConfiguration.queryExecutor).isEqualTo(executor)
+        assertThat(db.databaseConfiguration.transactionExecutor).isEqualTo(executor)
     }
 
     @Test
@@ -94,8 +97,8 @@
             mock(), TestDatabase::class.java, "foo"
         ).setTransactionExecutor(executor).build()
 
-        assertThat(db.mDatabaseConfiguration.queryExecutor).isEqualTo(executor)
-        assertThat(db.mDatabaseConfiguration.transactionExecutor).isEqualTo(executor)
+        assertThat(db.databaseConfiguration.queryExecutor).isEqualTo(executor)
+        assertThat(db.databaseConfiguration.transactionExecutor).isEqualTo(executor)
     }
 
     @Test
@@ -106,8 +109,48 @@
             mock(), TestDatabase::class.java, "foo"
         ).setQueryExecutor(executor1).setTransactionExecutor(executor2).build()
 
-        assertThat(db.mDatabaseConfiguration.queryExecutor).isEqualTo(executor1)
-        assertThat(db.mDatabaseConfiguration.transactionExecutor).isEqualTo(executor2)
+        assertThat(db.databaseConfiguration.queryExecutor).isEqualTo(executor1)
+        assertThat(db.databaseConfiguration.transactionExecutor).isEqualTo(executor2)
+    }
+
+    @Test
+    fun executors_setCoroutineContext() {
+        assertThrows<IllegalArgumentException> {
+            databaseBuilder(
+                mock(), TestDatabase::class.java, "foo"
+            ).setQueryCoroutineContext(Dispatchers.IO).setTransactionExecutor(mock()).build()
+        }.hasMessageThat()
+            .contains("This builder has already been configured with a CoroutineContext.")
+    }
+
+    @Test
+    fun coroutineContext_setQueryExecutor() {
+        assertThrows<IllegalArgumentException> {
+            databaseBuilder(
+                mock(), TestDatabase::class.java, "foo"
+            ).setQueryExecutor(mock()).setQueryCoroutineContext(Dispatchers.IO).build()
+        }.hasMessageThat()
+            .contains("This builder has already been configured with an Executor.")
+    }
+
+    @Test
+    fun coroutineContext_setTransactionExecutor() {
+        assertThrows<IllegalArgumentException> {
+            databaseBuilder(
+                mock(), TestDatabase::class.java, "foo"
+            ).setTransactionExecutor(mock()).setQueryCoroutineContext(Dispatchers.IO).build()
+        }.hasMessageThat()
+            .contains("This builder has already been configured with an Executor.")
+    }
+
+    @Test
+    fun coroutineContext_missingDispatcher() {
+        assertThrows<IllegalArgumentException> {
+            databaseBuilder(
+                mock(), TestDatabase::class.java, "foo"
+            ).setQueryCoroutineContext(EmptyCoroutineContext).build()
+        }.hasMessageThat()
+            .contains("It is required that the coroutine context contain a dispatcher.")
     }
 
     @Test
@@ -325,7 +368,7 @@
         )
             .addMigrations(EmptyMigration(1, 0))
             .build() as BuilderTest_TestDatabase_Impl
-        val config: DatabaseConfiguration = db.mDatabaseConfiguration
+        val config: DatabaseConfiguration = db.databaseConfiguration
         assertThat(
             config.migrationContainer.findMigrationPath(1, 2)).isEqualTo((db.mAutoMigrations)
         )
@@ -488,10 +531,10 @@
     }
 
     internal abstract class TestDatabase : RoomDatabase() {
-        lateinit var mDatabaseConfiguration: DatabaseConfiguration
+        lateinit var databaseConfiguration: DatabaseConfiguration
         override fun init(configuration: DatabaseConfiguration) {
             super.init(configuration)
-            mDatabaseConfiguration = configuration
+            databaseConfiguration = configuration
         }
     }
 
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/CoroutinesRoomTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/CoroutinesRoomTest.kt
deleted file mode 100644
index b9437f3..0000000
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/CoroutinesRoomTest.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.room
-
-import androidx.kruth.assertThat
-import kotlin.coroutines.ContinuationInterceptor
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.async
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.yield
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class CoroutinesRoomTest {
-
-    private val database = TestDatabase()
-    private val invalidationTracker = database.invalidationTracker as TestInvalidationTracker
-
-    @Test
-    fun testCreateFlow() = testRun {
-        var callableExecuted = false
-        val expectedResult = Any()
-        val flow = CoroutinesRoom.createFlow(
-            db = database,
-            inTransaction = false,
-            tableNames = arrayOf("Pet"),
-            callable = {
-                callableExecuted = true
-                expectedResult
-            }
-        )
-
-        assertThat(invalidationTracker.observers.isEmpty()).isTrue()
-        assertThat(callableExecuted).isFalse()
-
-        val job = async {
-            flow.first()
-        }
-        yield(); yield() // yield for async and flow
-
-        assertThat(invalidationTracker.observers).hasSize(1)
-        assertThat(callableExecuted).isTrue()
-
-        assertThat(job.await()).isEqualTo(expectedResult)
-        assertThat(invalidationTracker.observers).isEmpty()
-    }
-
-    // Use runBlocking dispatcher as query dispatchers, keeps the tests consistent.
-    private fun testRun(block: suspend CoroutineScope.() -> Unit) = runBlocking {
-        database.backingFieldMap["QueryDispatcher"] = coroutineContext[ContinuationInterceptor]!!
-        block.invoke(this)
-    }
-
-    private class TestDatabase : RoomDatabase() {
-        override fun createInvalidationTracker(): InvalidationTracker {
-            return TestInvalidationTracker(this)
-        }
-
-        override fun clearAllTables() {
-            throw UnsupportedOperationException("Shouldn't be called!")
-        }
-    }
-
-    private class TestInvalidationTracker(db: RoomDatabase) : InvalidationTracker(db) {
-        val observers = mutableListOf<Observer>()
-
-        override fun addObserver(observer: Observer) {
-            observers.add(observer)
-        }
-
-        override fun removeObserver(observer: Observer) {
-            observers.remove(observer)
-        }
-    }
-}
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 317eccd..90eb6d5 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
@@ -15,352 +15,264 @@
  */
 package androidx.room
 
-import android.annotation.SuppressLint
-import android.database.Cursor
-import android.database.sqlite.SQLiteException
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.arch.core.executor.ArchTaskExecutor
-import androidx.arch.core.executor.JunitTaskExecutorRule
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule
 import androidx.kruth.assertThat
-import androidx.kruth.assertWithMessage
-import androidx.sqlite.db.SimpleSQLiteQuery
-import androidx.sqlite.db.SupportSQLiteDatabase
-import androidx.sqlite.db.SupportSQLiteOpenHelper
-import androidx.sqlite.db.SupportSQLiteStatement
-import java.lang.ref.ReferenceQueue
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
 import java.lang.ref.WeakReference
 import java.util.Locale
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.concurrent.locks.ReentrantLock
 import kotlin.test.assertFailsWith
-import kotlin.test.fail
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withTimeout
 import org.junit.After
+import org.junit.AssumptionViolatedException
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.kotlin.KArgumentCaptor
-import org.mockito.kotlin.any
-import org.mockito.kotlin.argThat
-import org.mockito.kotlin.argumentCaptor
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.doThrow
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.isNull
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.reset
-import org.mockito.kotlin.times
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
-import org.mockito.stubbing.Answer
 
 @RunWith(JUnit4::class)
 class InvalidationTrackerTest {
-    private lateinit var mTracker: InvalidationTracker
 
-    private val mRoomDatabase: RoomDatabase = mock()
-
-    private val mSqliteDb: SupportSQLiteDatabase = mock()
-
-    private val mOpenHelper: SupportSQLiteOpenHelper = mock()
+    private lateinit var tracker: InvalidationTracker
+    private lateinit var sqliteDriver: FakeSQLiteDriver
+    private lateinit var roomDatabase: FakeRoomDatabase
 
     @get:Rule
-    var mTaskExecutorRule = JunitTaskExecutorRule(1, true)
+    val taskExecutorRule = CountingTaskExecutorRule()
 
     @Before
-    fun setup() {
-        val statement: SupportSQLiteStatement = mock()
-        doReturn(statement).whenever(mSqliteDb)
-            .compileStatement(eq(InvalidationTracker.RESET_UPDATED_TABLES_SQL))
-        doReturn(mSqliteDb).whenever(mOpenHelper).writableDatabase
-        doReturn(true).whenever(mRoomDatabase).isOpenInternal
-        doReturn(ArchTaskExecutor.getIOThreadExecutor()).whenever(mRoomDatabase).queryExecutor
-        val closeLock = ReentrantLock()
-        doReturn(closeLock).whenever(mRoomDatabase).getCloseLock()
-        doReturn(mOpenHelper).whenever(mRoomDatabase).openHelper
-        val shadowTables = HashMap<String, String>()
-        shadowTables["C"] = "C_content"
-        shadowTables["d"] = "a"
-        val viewTables = HashMap<String, Set<String>>()
-        val tableSet = HashSet<String>()
-        tableSet.add("a")
-        viewTables["e"] = tableSet
-        mTracker = InvalidationTracker(
-            mRoomDatabase, shadowTables, viewTables,
-            "a", "B", "i", "C", "d"
-        )
-        @Suppress("DEPRECATION")
-        mTracker.internalInit(mSqliteDb)
-        reset(mSqliteDb)
-    }
-
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    @Before
-    fun setLocale() {
+    fun setup() {
         Locale.setDefault(Locale.forLanguageTag("tr-TR"))
+
+        val shadowTables = buildMap {
+            put("C", "C_content")
+            put("d", "a")
+        }
+        val viewTables = buildMap {
+            put("e", setOf("a"))
+        }
+        val tableNames = arrayOf("a", "B", "i", "C", "d")
+        sqliteDriver = FakeSQLiteDriver()
+        roomDatabase = FakeRoomDatabase(
+            shadowTables,
+            viewTables,
+            tableNames
+        )
+        roomDatabase.init(
+            DatabaseConfiguration(
+                context = mock(),
+                name = null,
+                sqliteOpenHelperFactory = null,
+                migrationContainer = RoomDatabase.MigrationContainer(),
+                callbacks = null,
+                allowMainThreadQueries = true,
+                journalMode = RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING,
+                queryExecutor = ArchTaskExecutor.getIOThreadExecutor(),
+                transactionExecutor = ArchTaskExecutor.getIOThreadExecutor(),
+                multiInstanceInvalidationServiceIntent = null,
+                requireMigration = true,
+                allowDestructiveMigrationOnDowngrade = false,
+                migrationNotRequiredFrom = null,
+                copyFromAssetPath = null,
+                copyFromFile = null,
+                copyFromInputStream = null,
+                prepackagedDatabaseCallback = null,
+                typeConverters = emptyList(),
+                autoMigrationSpecs = emptyList(),
+                allowDestructiveMigrationForAllTables = false,
+                sqliteDriver = sqliteDriver,
+                queryCoroutineContext = null,
+            )
+        )
+        tracker = roomDatabase.invalidationTracker
     }
 
     @After
-    fun unsetLocale() {
+    fun after() {
         Locale.setDefault(Locale.US)
+
+        taskExecutorRule.drainTasks(1, TimeUnit.SECONDS)
+        assertThat(taskExecutorRule.isIdle).isTrue()
     }
 
     @Test
-    fun tableIds() {
-        assertThat(mTracker.tableIdLookup.size).isEqualTo(5)
-        assertThat(mTracker.tableIdLookup["a"]).isEqualTo(0)
-        assertThat(mTracker.tableIdLookup["b"]).isEqualTo(1)
-        assertThat(mTracker.tableIdLookup["i"]).isEqualTo(2)
-        assertThat(mTracker.tableIdLookup["c"]).isEqualTo(3) // fts
-        assertThat(mTracker.tableIdLookup["d"]).isEqualTo(0) // external content fts
-    }
-
-    @Test
-    fun tableNames() {
-        assertThat(mTracker.tablesNames.size).isEqualTo(5)
-        assertThat(mTracker.tablesNames[0]).isEqualTo("a")
-        assertThat(mTracker.tablesNames[1]).isEqualTo("b")
-        assertThat(mTracker.tablesNames[2]).isEqualTo("i")
-        assertThat(mTracker.tablesNames[3]).isEqualTo("c_content") // fts
-        assertThat(mTracker.tablesNames[4]).isEqualTo("a") // external content fts
-    }
-
-    @Test
-    @org.junit.Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
-    fun testWeak() {
-        val data = AtomicInteger(0)
-        var observer: InvalidationTracker.Observer? = object : InvalidationTracker.Observer("a") {
-            override fun onInvalidated(tables: Set<String>) {
-                data.incrementAndGet()
-            }
-        }
-        val queue = ReferenceQueue<Any?>()
-        WeakReference(observer, queue)
-        mTracker.addWeakObserver(observer!!)
-        setInvalidatedTables(0)
-        refreshSync()
-        assertThat(data.get()).isEqualTo(1)
-        @Suppress("UNUSED_VALUE") // On purpose, to dereference the observer and GC it
-        observer = null
-        forceGc(queue)
-        setInvalidatedTables(0)
-        refreshSync()
-        assertThat(data.get()).isEqualTo(1)
-    }
-
-    @Test
-    fun addRemoveObserver() {
-        val observer: InvalidationTracker.Observer = LatchObserver(1, "a")
-        mTracker.addObserver(observer)
-        assertThat(mTracker.observerMap.size()).isEqualTo(1)
-        mTracker.removeObserver(LatchObserver(1, "a"))
-        assertThat(mTracker.observerMap.size()).isEqualTo(1)
-        mTracker.removeObserver(observer)
-        assertThat(mTracker.observerMap.size()).isEqualTo(0)
-    }
-
-    private fun drainTasks() {
-        mTaskExecutorRule.drainTasks(200)
-    }
-
-    @Test
-    fun badObserver() {
+    fun observerWithNoExistingTable() = runTest {
         assertFailsWith<IllegalArgumentException>(message = "There is no table with name x") {
             val observer: InvalidationTracker.Observer = LatchObserver(1, "x")
-            mTracker.addObserver(observer)
+            tracker.subscribe(observer)
         }
     }
 
-    private fun refreshSync() {
-        mTracker.refreshVersionsAsync()
-        drainTasks()
-    }
-
-    @Ignore // b/253058904
     @Test
-    fun refreshCheckTasks() {
-        whenever(mRoomDatabase.query(any<SimpleSQLiteQuery>(), isNull())).thenReturn(mock<Cursor>())
-        mTracker.refreshVersionsAsync()
-        mTracker.refreshVersionsAsync()
-        verify(mTaskExecutorRule.taskExecutor).executeOnDiskIO(mTracker.refreshRunnable)
-        drainTasks()
-        reset(mTaskExecutorRule.taskExecutor)
-        mTracker.refreshVersionsAsync()
-        verify(mTaskExecutorRule.taskExecutor).executeOnDiskIO(mTracker.refreshRunnable)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun observe1Table() {
-        val observer = LatchObserver(1, "a")
-        mTracker.addObserver(observer)
-        setInvalidatedTables(0)
-        refreshSync()
-        assertThat(observer.await()).isEqualTo(true)
-        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
-        assertThat(observer.invalidatedTables).contains("a")
-        setInvalidatedTables(1)
-        observer.reset(1)
-        refreshSync()
-        assertThat(observer.await()).isEqualTo(false)
-        setInvalidatedTables(0)
-        refreshSync()
-        assertThat(observer.await()).isEqualTo(true)
-        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
-        assertThat(observer.invalidatedTables).contains("a")
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun observe2Tables() {
-        val observer = LatchObserver(1, "A", "B")
-        mTracker.addObserver(observer)
-        setInvalidatedTables(0, 1)
-        refreshSync()
-        assertThat(observer.await()).isEqualTo(true)
-        assertThat(observer.invalidatedTables!!.size).isEqualTo(2)
-        assertThat(observer.invalidatedTables).containsAtLeast("A", "B")
-        setInvalidatedTables(1, 2)
-        observer.reset(1)
-        refreshSync()
-        assertThat(observer.await()).isEqualTo(true)
-        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
-        assertThat(observer.invalidatedTables).contains("B")
-        setInvalidatedTables(0, 3)
-        observer.reset(1)
-        refreshSync()
-        assertThat(observer.await()).isEqualTo(true)
-        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
-        assertThat(observer.invalidatedTables).contains("A")
-        observer.reset(1)
-        refreshSync()
-        assertThat(observer.await()).isEqualTo(false)
-    }
-
-    @Test
-    fun locale() {
+    fun ignoreCaseInTableNames() {
         val observer = LatchObserver(1, "I")
-        mTracker.addObserver(observer)
+        tracker.addObserver(observer)
     }
 
     @Test
-    fun closedDb() {
-        doReturn(false).whenever(mRoomDatabase).isOpenInternal
-        doThrow(IllegalStateException("foo")).whenever(mOpenHelper).writableDatabase
-        mTracker.addObserver(LatchObserver(1, "a", "b"))
-        mTracker.refreshRunnable.run()
-    }
+    fun observeOneTable() = runTest {
+        val observer = LatchObserver(1, "a")
+        tracker.subscribe(observer)
 
-    @Test
-    fun createTriggerOnShadowTable() {
-        val observer = LatchObserver(1, "C")
-        val triggers = arrayOf("UPDATE", "DELETE", "INSERT")
-        var sqlCaptorValues: List<String>
-        mTracker.addObserver(observer)
-        var sqlArgCaptor: KArgumentCaptor<String> = argumentCaptor()
-        verify(mSqliteDb, times(4)).execSQL(sqlArgCaptor.capture())
-        sqlCaptorValues = sqlArgCaptor.allValues
-        assertThat(sqlCaptorValues[0])
-            .isEqualTo("INSERT OR IGNORE INTO room_table_modification_log VALUES(3, 0)")
-        for (i in triggers.indices) {
-            assertThat(sqlCaptorValues[i + 1])
-                .isEqualTo(
-                    "CREATE TEMP TRIGGER IF NOT EXISTS " +
-                        "`room_table_modification_trigger_c_content_" + triggers[i] +
-                        "` AFTER " + triggers[i] + " ON `c_content` BEGIN UPDATE " +
-                        "room_table_modification_log SET invalidated = 1 WHERE table_id = 3 " +
-                        "AND invalidated = 0; END"
-                )
-        }
-        reset(mSqliteDb)
-        mTracker.removeObserver(observer)
-        sqlArgCaptor = argumentCaptor()
-        verify(mSqliteDb, times(3)).execSQL(sqlArgCaptor.capture())
-        sqlCaptorValues = sqlArgCaptor.allValues
-        for (i in triggers.indices) {
-            assertThat(sqlCaptorValues[i])
-                .isEqualTo(
-                    "DROP TRIGGER IF EXISTS `room_table_modification_trigger_c_content_" +
-                        triggers[i] + "`"
-                )
-        }
-    }
-
-    @Test
-    fun observeFtsTable() {
-        val observer = LatchObserver(1, "C")
-        mTracker.addObserver(observer)
-        setInvalidatedTables(3)
-        refreshSync()
+        // Mark 'a' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(true)
         assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
-        assertThat(observer.invalidatedTables).contains("C")
-        setInvalidatedTables(1)
+        assertThat(observer.invalidatedTables).containsExactly("a")
+
+        // Mark 'B' as invalidated and expect no notification
         observer.reset(1)
-        refreshSync()
+        sqliteDriver.setInvalidatedTables(1)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(false)
-        setInvalidatedTables(0, 3)
-        refreshSync()
+
+        // Mark 'a' as invalidated again and expect a new notification
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(true)
+        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+        assertThat(observer.invalidatedTables).containsExactly("a")
+    }
+
+    @Test
+    fun observeTwoTables() = runTest {
+        val observer = LatchObserver(1, "A", "B")
+        tracker.subscribe(observer)
+
+        // Mark 'a' and 'B' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(0, 1)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(true)
+        assertThat(observer.invalidatedTables!!.size).isEqualTo(2)
+        assertThat(observer.invalidatedTables).containsExactly("A", "B")
+
+        // Mark 'B' and 'i' as invalidated and expect a notification
+        observer.reset(1)
+        sqliteDriver.setInvalidatedTables(1, 2)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(true)
+        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+        assertThat(observer.invalidatedTables).containsExactly("B")
+
+        // Mark 'a' and 'i' as invalidated and expect a notification
+        observer.reset(1)
+        sqliteDriver.setInvalidatedTables(0, 3)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(true)
+        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+        assertThat(observer.invalidatedTables).containsExactly("A")
+
+        // Do a sync without any invalidation and expect no notification
+        observer.reset(1)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(false)
+    }
+
+    @Test
+    fun observeFtsTable() = runTest {
+        val observer = LatchObserver(1, "C")
+        tracker.subscribe(observer)
+
+        // Mark 'C' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(3)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(true)
+        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+        assertThat(observer.invalidatedTables).contains("C")
+
+        // Mark 'a' as invalidated and expect no notification
+        sqliteDriver.setInvalidatedTables(1)
+        observer.reset(1)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(false)
+
+        // Mark 'a' and 'C' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(0, 3)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(true)
         assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
         assertThat(observer.invalidatedTables).contains("C")
     }
 
     @Test
-    fun observeExternalContentFtsTable() {
+    fun observeExternalContentFtsTable() = runTest {
         val observer = LatchObserver(1, "d")
-        mTracker.addObserver(observer)
-        setInvalidatedTables(0)
-        refreshSync()
+        tracker.subscribe(observer)
+
+        // Mark 'a' as invalidated and expect a notification, 'a' is the content table of 'd'
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(true)
         assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
         assertThat(observer.invalidatedTables).contains("d")
-        setInvalidatedTables(2, 3)
+
+        // Mark 'i' and 'C' as invalidated and expect no notification
+        sqliteDriver.setInvalidatedTables(2, 3)
         observer.reset(1)
-        refreshSync()
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(false)
-        setInvalidatedTables(0, 1)
-        refreshSync()
+
+        // Mark 'a' and 'B' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(0, 1)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(true)
         assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
         assertThat(observer.invalidatedTables).contains("d")
     }
 
     @Test
-    fun observeExternalContentFtsTableAndContentTable() {
+    fun observeExternalContentFtsTableAndContentTable() = runTest {
         val observer = LatchObserver(1, "d", "a")
-        mTracker.addObserver(observer)
-        setInvalidatedTables(0)
-        refreshSync()
+        tracker.subscribe(observer)
+
+        // Mark 'a' as invalidated and expect a notification of both 'a' and 'd' since 'd' is
+        // backed by 'a'
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(true)
         assertThat(observer.invalidatedTables!!.size).isEqualTo(2)
         assertThat(observer.invalidatedTables).containsAtLeast("d", "a")
-        setInvalidatedTables(2, 3)
+
+        // Mark 'B' as invalidated and expect no notification
         observer.reset(1)
-        refreshSync()
+        sqliteDriver.setInvalidatedTables(2, 3)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(false)
-        setInvalidatedTables(0, 1)
-        refreshSync()
+
+        // Mark 'a' and 'B' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(0, 1)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(true)
         assertThat(observer.invalidatedTables!!.size).isEqualTo(2)
         assertThat(observer.invalidatedTables).containsAtLeast("d", "a")
     }
 
     @Test
-    fun observeExternalContentFatsTableAndContentTableSeparately() {
+    fun observeExternalContentFatsTableAndContentTableSeparately() = runTest {
         val observerA = LatchObserver(1, "a")
         val observerD = LatchObserver(1, "d")
-        mTracker.addObserver(observerA)
-        mTracker.addObserver(observerD)
-        setInvalidatedTables(0)
-        refreshSync()
+        tracker.subscribe(observerA)
+        tracker.subscribe(observerD)
+
+        // Mark 'a' as invalidated and expect a notification of both 'a' and 'd' since 'a' is
+        // the content table for 'd'
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
         assertThat(observerA.await()).isEqualTo(true)
         assertThat(observerD.await()).isEqualTo(true)
         assertThat(observerA.invalidatedTables!!.size).isEqualTo(1)
@@ -369,11 +281,12 @@
         assertThat(observerD.invalidatedTables).contains("d")
 
         // Remove observer 'd' which is backed by 'a', observers to 'a' should still work.
-        mTracker.removeObserver(observerD)
-        setInvalidatedTables(0)
+        tracker.removeObserver(observerD)
         observerA.reset(1)
         observerD.reset(1)
-        refreshSync()
+        // Mark 'a' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
         assertThat(observerA.await()).isEqualTo(true)
         assertThat(observerD.await()).isEqualTo(false)
         assertThat(observerA.invalidatedTables!!.size).isEqualTo(1)
@@ -381,142 +294,381 @@
     }
 
     @Test
-    fun observeView() {
+    fun observeView() = runTest {
         val observer = LatchObserver(1, "E")
-        mTracker.addObserver(observer)
-        setInvalidatedTables(0, 1)
-        refreshSync()
+        tracker.subscribe(observer)
+
+        // Mark 'a' and 'B' as invalidated and expect a notification, the view 'E' is backed by 'a'
+        sqliteDriver.setInvalidatedTables(0, 1)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(true)
         assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
         assertThat(observer.invalidatedTables).contains("a")
-        setInvalidatedTables(2, 3)
+
+        // Mark 'B' and 'i' as invalidated and expect no notification
         observer.reset(1)
-        refreshSync()
+        sqliteDriver.setInvalidatedTables(2, 3)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(false)
-        setInvalidatedTables(0, 1)
-        refreshSync()
+
+        // Mark 'a' and 'B' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(0, 1)
+        tracker.awaitRefreshAsync()
         assertThat(observer.await()).isEqualTo(true)
         assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
         assertThat(observer.invalidatedTables).contains("a")
     }
 
     @Test
-    fun failFastCreateLiveData() {
-        // assert that sending a bad createLiveData table name fails instantly
-        try {
-            mTracker.createLiveData<Unit>(
-                tableNames = arrayOf("invalid table name"),
+    fun multipleRefreshAsync() = runTest {
+        // Validate that when multiple refresh are enqueued, that only one runs.
+        tracker.refreshAsync()
+        tracker.refreshAsync()
+        tracker.refreshAsync()
+
+        taskExecutorRule.drainTasks(1, TimeUnit.SECONDS)
+
+        assertThat(
+            sqliteDriver.preparedQueries.filter {
+                it == SELECT_INVALIDATED_QUERY
+            }
+        ).hasSize(1)
+    }
+
+    @Test
+    fun refreshAndCloseDb() = runTest {
+        // Validates that closing the database with a pending refresh is OK
+        tracker.refreshAsync()
+        roomDatabase.close()
+    }
+
+    @Test
+    fun closeDbAndRefresh() = runTest {
+        // Validates that closing the database and then somehow refreshing is OK
+        roomDatabase.close()
+        tracker.refreshAsync()
+    }
+
+    @Test
+    fun refreshAndCloseDbWithSlowObserver() = runTest {
+        // Validates that a slow observer will finish notification after database closing
+        val invalidatedLatch = CountDownLatch(1)
+        val invalidated = atomic(false)
+        tracker.addObserver(object : InvalidationTracker.Observer("a") {
+            override fun onInvalidated(tables: Set<String>) {
+                invalidatedLatch.countDown()
+                assertThat(invalidated.compareAndSet(expect = false, update = true)).isTrue()
+                runBlocking { delay(100) }
+            }
+        })
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.refreshAsync()
+        taskExecutorRule.drainTasks(200, TimeUnit.MILLISECONDS)
+        invalidatedLatch.await()
+        roomDatabase.close()
+        assertThat(invalidated.value).isTrue()
+    }
+
+    @Test
+    fun createTriggerOnTable() = runTest {
+        // Note: This tests validate triggers that are an impl (but important)
+        // detail of the tracker, but in theory this is already covered by tests with observers
+        val triggers = listOf("INSERT", "UPDATE", "DELETE")
+
+        val observer = LatchObserver(1, "a")
+        tracker.subscribe(observer)
+        tracker.sync()
+
+        // Verifies the 'invalidated' column is reset when tracking starts
+        assertThat(sqliteDriver.preparedQueries).contains(
+            "INSERT OR IGNORE INTO room_table_modification_log VALUES(0, 0)"
+        )
+        // Verifies triggers created for observed table
+        triggers.forEach { trigger ->
+            assertThat(sqliteDriver.preparedQueries).contains(
+                "CREATE TEMP TRIGGER IF NOT EXISTS " +
+                    "`room_table_modification_trigger_a_$trigger` " +
+                    "AFTER $trigger ON `a` BEGIN UPDATE " +
+                    "room_table_modification_log SET invalidated = 1 WHERE table_id = 0 " +
+                    "AND invalidated = 0; END"
+            )
+        }
+
+        tracker.unsubscribe(observer)
+        tracker.sync()
+        triggers.forEach { trigger ->
+            assertThat(sqliteDriver.preparedQueries).contains(
+                "DROP TRIGGER IF EXISTS `room_table_modification_trigger_a_$trigger`"
+            )
+        }
+    }
+
+    @Test
+    fun createTriggerOnShadowTable() = runTest {
+        // Note: This tests validate triggers that are an impl (but important)
+        // detail of the tracker, but in theory this is already covered by tests with observers
+        val triggers = listOf("INSERT", "UPDATE", "DELETE")
+
+        val observer = LatchObserver(1, "C")
+        tracker.subscribe(observer)
+        tracker.sync()
+
+        // Verifies the 'invalidated' column is reset when tracking starts
+        assertThat(sqliteDriver.preparedQueries).contains(
+            "INSERT OR IGNORE INTO room_table_modification_log VALUES(3, 0)"
+        )
+        // Verifies that when tracking a table ('C') that has an external content table
+        // that triggers are installed in the content table and not the virtual table
+        triggers.forEach { trigger ->
+            assertThat(sqliteDriver.preparedQueries).contains(
+                "CREATE TEMP TRIGGER IF NOT EXISTS " +
+                    "`room_table_modification_trigger_c_content_$trigger` " +
+                    "AFTER $trigger ON `c_content` BEGIN UPDATE " +
+                    "room_table_modification_log SET invalidated = 1 WHERE table_id = 3 " +
+                    "AND invalidated = 0; END"
+            )
+        }
+
+        tracker.unsubscribe(observer)
+        tracker.sync()
+        // Validates trigger are removed when tracking stops
+        triggers.forEach { trigger ->
+            assertThat(sqliteDriver.preparedQueries).contains(
+                "DROP TRIGGER IF EXISTS `room_table_modification_trigger_c_content_$trigger`"
+            )
+        }
+    }
+
+    @Test
+    fun createLiveDataWithNoExistingTable() {
+        // Validate that sending a bad createLiveData table name fails quickly
+        assertFailsWith<IllegalArgumentException>(message = "There is no table with name x") {
+            tracker.createLiveData(
+                tableNames = arrayOf("x"),
                 inTransaction = false
             ) {}
-            fail("should've throw an exception for invalid table name")
-        } catch (expected: IllegalArgumentException) {
-            // expected
         }
     }
 
     @Test
-    fun closedDbAfterOpen() {
-        setInvalidatedTables(3, 1)
-        mTracker.addObserver(LatchObserver(1, "a", "b"))
-        mTracker.syncTriggers()
-        mTracker.refreshRunnable.run()
-        doThrow(SQLiteException("foo")).whenever(mRoomDatabase)?.query(
-            query = InvalidationTracker.SELECT_UPDATED_TABLES_SQL,
-            args = arrayOf(Array<Any>::class.java)
-        )
-        mTracker.pendingRefresh.set(true)
-        mTracker.refreshRunnable.run()
+    fun addAndRemoveObserver() = runTest {
+        val observer = LatchObserver(1, "a")
+        tracker.addObserver(observer)
+
+        // Mark 'a' as invalidated and expect a notification
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(true)
+        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+        assertThat(observer.invalidatedTables).containsExactly("a")
+
+        // Remove observer, validating tracking stops immediately
+        tracker.removeObserver(observer)
+
+        // Mark 'a' as invalidated and expect no notification
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
+        assertThat(observer.await()).isEqualTo(true)
+        assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+        assertThat(observer.invalidatedTables).containsExactly("a")
     }
 
-    /**
-     * Setup Cursor result to return INVALIDATED for given tableIds
-     */
-    private fun setInvalidatedTables(vararg tableIds: Int) {
-        // mockito does not like multi-threaded access so before setting versions, make sure we
-        // sync background tasks.
-        drainTasks()
-        val cursor = createCursorWithValues(*tableIds)
-        doReturn(cursor).whenever(mRoomDatabase)?.query(
-            query = argThat<SimpleSQLiteQuery> { argument ->
-                argument.sql == InvalidationTracker.SELECT_UPDATED_TABLES_SQL
-            },
-            signal = isNull(),
-        )
-    }
-
-    private fun createCursorWithValues(vararg tableIds: Int): Cursor {
-        val cursor: Cursor = mock()
-        val index = AtomicInteger(-1)
-        whenever(cursor.moveToNext()).thenAnswer { index.addAndGet(1) < tableIds.size }
-        val intAnswer = Answer { invocation ->
-            // checkUpdatedTable only checks for column 0 (invalidated table id)
-            assert(invocation.arguments[0] as Int == 0)
-            tableIds[index.toInt()]
+    @Test
+    fun weakObserver() {
+        val invalidated = atomic(0)
+        var observer: InvalidationTracker.Observer? = object : InvalidationTracker.Observer("a") {
+            override fun onInvalidated(tables: Set<String>) {
+                invalidated.incrementAndGet()
+            }
         }
-        whenever(cursor.getInt(anyInt())).thenAnswer(intAnswer)
-        return cursor
+        tracker.addWeakObserver(observer!!)
+
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
+        assertThat(invalidated.value).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
+        // the test result is ignored.
+        runBlocking {
+            try {
+                val weakRef = WeakReference(observer)
+                observer = null
+                withTimeout(TimeUnit.SECONDS.toMillis(2)) {
+                    while (true) {
+                        System.gc()
+                        if (weakRef.get() == null) {
+                            break
+                        }
+                        delay(10)
+                    }
+                }
+            } catch (ex: TimeoutCancellationException) {
+                throw AssumptionViolatedException(
+                    "Test was flaky due to involving garbage collector loop."
+                )
+            }
+        }
+
+        sqliteDriver.setInvalidatedTables(0)
+        tracker.awaitRefreshAsync()
+        assertThat(invalidated.value).isEqualTo(1)
     }
 
-    internal class LatchObserver(
+    private fun InvalidationTracker.awaitRefreshAsync() {
+        refreshAsync()
+        taskExecutorRule.drainTasks(200, TimeUnit.MILLISECONDS)
+    }
+
+    private class LatchObserver(
         count: Int,
         vararg tableNames: String
     ) : InvalidationTracker.Observer(arrayOf(*tableNames)) {
-        private var mLatch: CountDownLatch
+        private var latch: CountDownLatch
 
         var invalidatedTables: Set<String>? = null
             private set
 
         init {
-            mLatch = CountDownLatch(count)
+            latch = CountDownLatch(count)
         }
 
         fun await(): Boolean {
-            return mLatch.await(3, TimeUnit.SECONDS)
+            return latch.await(200, TimeUnit.MILLISECONDS)
         }
 
         override fun onInvalidated(tables: Set<String>) {
             invalidatedTables = tables
-            mLatch.countDown()
+            latch.countDown()
         }
 
         fun reset(count: Int) {
             invalidatedTables = null
-            mLatch = CountDownLatch(count)
+            latch = CountDownLatch(count)
+        }
+    }
+
+    private inner class FakeRoomDatabase(
+        private val shadowTablesMap: Map<String, String>,
+        private val viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
+        private val tableNames: Array<String>
+    ) : RoomDatabase() {
+
+        override fun createInvalidationTracker(): InvalidationTracker {
+            return InvalidationTracker(
+                this,
+                shadowTablesMap,
+                viewTables,
+                *tableNames
+            )
+        }
+
+        override fun createOpenDelegate(): RoomOpenDelegateMarker {
+            return object : RoomOpenDelegate(0, "") {
+                override fun onCreate(connection: SQLiteConnection) {}
+                override fun onPreMigrate(connection: SQLiteConnection) {}
+                override fun onValidateSchema(connection: SQLiteConnection) =
+                    ValidationResult(true, null)
+                override fun onPostMigrate(connection: SQLiteConnection) {}
+                override fun onOpen(connection: SQLiteConnection) {}
+                override fun createAllTables(connection: SQLiteConnection) {}
+                override fun dropAllTables(connection: SQLiteConnection) {}
+            }
+        }
+
+        override fun clearAllTables() {}
+    }
+
+    private class FakeSQLiteDriver : SQLiteDriver {
+
+        private val invalidateTablesQueue = mutableListOf<IntArray>()
+
+        val preparedQueries = mutableListOf<String>()
+
+        override fun open(): SQLiteConnection {
+            return FakeSQLiteConnection()
+        }
+
+        fun setInvalidatedTables(vararg tableIds: Int) {
+            invalidateTablesQueue.add(tableIds)
+        }
+
+        private inner class FakeSQLiteConnection : SQLiteConnection {
+
+            override fun prepare(sql: String): SQLiteStatement {
+                preparedQueries.add(sql)
+                val invalidatedTables =
+                    if (sql == SELECT_INVALIDATED_QUERY && invalidateTablesQueue.isNotEmpty()) {
+                        invalidateTablesQueue.removeFirst()
+                    } else {
+                        null
+                    }
+                return FakeSQLiteStatement(invalidatedTables)
+            }
+
+            override fun close() {
+            }
+        }
+
+        private inner class FakeSQLiteStatement(
+            private val invalidateTables: IntArray?
+        ) : SQLiteStatement {
+
+            private var position = -1
+
+            override fun bindBlob(index: Int, value: ByteArray) {}
+            override fun bindDouble(index: Int, value: Double) {}
+            override fun bindLong(index: Int, value: Long) {}
+            override fun bindText(index: Int, value: String) {}
+            override fun bindNull(index: Int) {}
+            override fun getBlob(index: Int): ByteArray {
+                error("Should not be called")
+            }
+
+            override fun getDouble(index: Int): Double {
+                error("Should not be called")
+            }
+
+            override fun getLong(index: Int): Long {
+                return if (invalidateTables != null) {
+                    invalidateTables[position].toLong()
+                } else {
+                    0L
+                }
+            }
+
+            override fun getText(index: Int): String {
+                error("Should not be called")
+            }
+
+            override fun isNull(index: Int): Boolean {
+                return false
+            }
+
+            override fun getColumnCount(): Int {
+                return 0
+            }
+
+            override fun getColumnName(index: Int): String {
+                error("Should not be called")
+            }
+
+            override fun step(): Boolean {
+                if (invalidateTables != null) {
+                    return ++position < invalidateTables.size
+                } else {
+                    return false
+                }
+            }
+
+            override fun reset() {}
+            override fun clearBindings() {}
+            override fun close() {}
         }
     }
 
     companion object {
-        /**
-         * Tries to trigger garbage collection by allocating in the heap until an element is
-         * available in the given reference queue.
-         */
-        @SuppressLint("BanThreadSleep")
-        private fun forceGc(queue: ReferenceQueue<Any?>) {
-            val continueTriggeringGc = AtomicBoolean(true)
-            val t = Thread {
-                var byteCount = 0
-                try {
-                    val leak = ArrayList<ByteArray>()
-                    do {
-                        val arraySize = (Math.random() * 1000).toInt()
-                        byteCount += arraySize
-                        leak.add(ByteArray(arraySize))
-                        System.gc() // Not guaranteed to trigger GC, hence the leak and the timeout
-                        Thread.sleep(10)
-                    } while (continueTriggeringGc.get())
-                } catch (e: InterruptedException) {
-                    // Ignored
-                }
-                println("Allocated $byteCount bytes trying to force a GC.")
-            }
-            t.start()
-            val result = queue.remove(TimeUnit.SECONDS.toMillis(10))
-            continueTriggeringGc.set(false)
-            t.interrupt()
-            assertWithMessage("Couldn't trigger garbage collection, test flake")
-                .that(result)
-                .isNotNull()
-            result.clear()
-        }
+        private const val SELECT_INVALIDATED_QUERY =
+            "SELECT * FROM room_table_modification_log WHERE invalidated = 1"
     }
 }
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/ObservedTableStatesTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/ObservedTableStatesTest.kt
new file mode 100644
index 0000000..be54206
--- /dev/null
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/ObservedTableStatesTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room
+
+import androidx.kruth.assertThat
+import androidx.room.ObservedTableStates.ObserveOp
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertNull
+import kotlinx.coroutines.test.runTest
+
+class ObservedTableStatesTest {
+    private lateinit var tableStates: ObservedTableStates
+
+    @BeforeTest
+    fun setup() {
+        tableStates = ObservedTableStates(TABLE_COUNT)
+    }
+
+    @Test
+    fun basicAdd() = runTest {
+        tableStates.onObserverAdded(intArrayOf(2, 3))
+        assertThat(tableStates.getTablesToSync()).isEqualTo(
+            createSyncResult(
+                mapOf(2 to ObserveOp.ADD, 3 to ObserveOp.ADD)
+            )
+        )
+    }
+
+    @Test
+    fun basicRemove() = runTest {
+        tableStates.onObserverAdded(intArrayOf(2, 3))
+        tableStates.getTablesToSync()
+
+        tableStates.onObserverRemoved(intArrayOf(3))
+        assertThat(tableStates.getTablesToSync()).isEqualTo(
+            createSyncResult(
+                mapOf(3 to ObserveOp.REMOVE)
+            )
+        )
+    }
+
+    @Test
+    fun noChange() = runTest {
+        tableStates.onObserverAdded(intArrayOf(1, 3))
+        tableStates.getTablesToSync()
+
+        tableStates.onObserverAdded(intArrayOf(3))
+        assertNull(tableStates.getTablesToSync())
+    }
+
+    @Test
+    fun multipleAdditionsDeletions() = runTest {
+        tableStates.onObserverAdded(intArrayOf(2, 4))
+        tableStates.getTablesToSync()
+
+        tableStates.onObserverAdded(intArrayOf(2))
+        assertNull(tableStates.getTablesToSync())
+
+        tableStates.onObserverAdded(intArrayOf(2, 4))
+        assertNull(tableStates.getTablesToSync())
+
+        tableStates.onObserverRemoved(intArrayOf(2))
+        assertNull(tableStates.getTablesToSync())
+
+        tableStates.onObserverRemoved(intArrayOf(2, 4))
+        assertNull(tableStates.getTablesToSync())
+
+        tableStates.onObserverAdded(intArrayOf(1, 3))
+        tableStates.onObserverRemoved(intArrayOf(2, 4))
+        assertThat(tableStates.getTablesToSync()).isEqualTo(
+            createSyncResult(
+                mapOf(
+                    1 to ObserveOp.ADD,
+                    2 to ObserveOp.REMOVE,
+                    3 to ObserveOp.ADD,
+                    4 to ObserveOp.REMOVE
+                )
+            )
+        )
+    }
+
+    companion object {
+        private const val TABLE_COUNT = 5
+
+        private fun createSyncResult(tuples: Map<Int, ObserveOp>): Array<ObserveOp> {
+            return Array(TABLE_COUNT) { i ->
+                tuples[i] ?: ObserveOp.NO_OP
+            }
+        }
+    }
+}
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/ObservedTableTrackerTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/ObservedTableTrackerTest.kt
deleted file mode 100644
index 098d1b4..0000000
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/ObservedTableTrackerTest.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.room
-
-import androidx.kruth.assertThat
-import java.util.Arrays
-import kotlin.test.assertNull
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ObservedTableTrackerTest {
-    private lateinit var mTracker: InvalidationTracker.ObservedTableTracker
-    @Before
-    fun setup() {
-        mTracker = InvalidationTracker.ObservedTableTracker(TABLE_COUNT)
-    }
-
-    @Test
-    fun basicAdd() {
-        mTracker.onAdded(2, 3)
-        assertThat(
-            mTracker.getTablesToSync()
-        ).isEqualTo(
-            createResponse(
-                2,
-                InvalidationTracker.ObservedTableTracker.ADD,
-                3,
-                InvalidationTracker.ObservedTableTracker.ADD
-            )
-        )
-    }
-
-    @Test
-    fun basicRemove() {
-        initState(2, 3)
-        mTracker.onRemoved(3)
-        assertThat(
-            mTracker.getTablesToSync()
-        ).isEqualTo(
-            createResponse(3, InvalidationTracker.ObservedTableTracker.REMOVE)
-        )
-    }
-
-    @Test
-    fun noChange() {
-        initState(1, 3)
-        mTracker.onAdded(3)
-        assertNull(
-            mTracker.getTablesToSync()
-        )
-    }
-
-    @Test
-    fun multipleAdditionsDeletions() {
-        initState(2, 4)
-        mTracker.onAdded(2)
-        assertNull(
-            mTracker.getTablesToSync()
-        )
-        mTracker.onAdded(2, 4)
-        assertNull(
-            mTracker.getTablesToSync()
-        )
-        mTracker.onRemoved(2)
-        assertNull(
-            mTracker.getTablesToSync()
-        )
-        mTracker.onRemoved(2, 4)
-        assertNull(
-            mTracker.getTablesToSync()
-        )
-        mTracker.onAdded(1, 3)
-        mTracker.onRemoved(2, 4)
-        assertThat(
-            mTracker.getTablesToSync()
-        ).isEqualTo(
-            createResponse(
-                1,
-                InvalidationTracker.ObservedTableTracker.ADD,
-                2,
-                InvalidationTracker.ObservedTableTracker.REMOVE,
-                3,
-                InvalidationTracker.ObservedTableTracker.ADD,
-                4,
-                InvalidationTracker.ObservedTableTracker.REMOVE
-            )
-        )
-    }
-
-    private fun initState(vararg tableIds: Int) {
-        mTracker.onAdded(*tableIds)
-        mTracker.getTablesToSync()
-    }
-
-    companion object {
-        private const val TABLE_COUNT = 5
-        private fun createResponse(vararg tuples: Int): IntArray {
-            val result = IntArray(TABLE_COUNT)
-            Arrays.fill(result, InvalidationTracker.ObservedTableTracker.NO_OP)
-            var i = 0
-            while (i < tuples.size) {
-                result[tuples[i]] = tuples[i + 1]
-                i += 2
-            }
-            return result
-        }
-    }
-}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/DatabaseConfiguration.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/DatabaseConfiguration.kt
index 58f3fe1..f5a89e9 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/DatabaseConfiguration.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/DatabaseConfiguration.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.migration.AutoMigrationSpec
 import androidx.sqlite.SQLiteDriver
+import kotlin.coroutines.CoroutineContext
 
 /**
  * Configuration class for a [RoomDatabase].
@@ -35,4 +36,5 @@
     val typeConverters: List<Any>
     val autoMigrationSpecs: List<AutoMigrationSpec>
     val sqliteDriver: SQLiteDriver?
+    val queryCoroutineContext: CoroutineContext?
 }
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 87f71e6..8562ad9 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
@@ -17,12 +17,27 @@
 package androidx.room
 
 import androidx.annotation.RestrictTo
+import androidx.room.InvalidationTracker.Observer
+import androidx.room.Transactor.SQLiteTransactionType
+import androidx.room.concurrent.ifNotClosed
 import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteException
+import androidx.sqlite.execSQL
 import kotlin.jvm.JvmSuppressWildcards
+import kotlinx.atomicfu.atomic
+import kotlinx.atomicfu.locks.reentrantLock
+import kotlinx.atomicfu.locks.withLock
+import kotlinx.coroutines.launch
 
 /**
- * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * The invalidation tracker keeps track of tables modified by queries and notifies its subscribed
  * [Observer]s about such modifications.
+ *
+ * [Observer]s contain one or more tables and are added to the tracker via [subscribe]. Once
+ * an observer is subscribed, if a database operation changes one of the tables the observer is
+ * subscribed to, then such table is considered 'invalidated' and [Observer.onInvalidated] will
+ * be invoked on the observer. If an observer is no longer interested in tracking modifications
+ * it can be removed via [unsubscribe].
  */
 expect class InvalidationTracker
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@@ -33,7 +48,555 @@
     vararg tableNames: String
 ) {
     /**
-     * Internal method to initialize table tracking. Invoked by generated code.
+     * Internal method to initialize tracker for a given connection. Invoked by generated code.
      */
     internal fun internalInit(connection: SQLiteConnection)
+
+    /**
+     * Subscribes the given [observer] with the tracker such that it is notified if any table it
+     * is interested on changes.
+     *
+     * If the observer is already subscribed, then this function does nothing.
+     *
+     * @param observer The observer that will listen for database changes.
+     * @throws IllegalArgumentException if one of the tables in the observer does not exist.
+     */
+    // TODO(b/329315924): Replace with Flow based API
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    suspend fun subscribe(observer: Observer)
+
+    /**
+     * Unsubscribes the given [observer] from the tracker.
+     *
+     * If the observer was never subscribed in the first place, then this function does nothing.
+     *
+     * @param observer The observer to remove.
+     */
+    // TODO(b/329315924): Replace with Flow based API
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    suspend fun unsubscribe(observer: Observer)
+
+    /**
+     * Synchronize subscribed observers with their tables.
+     *
+     * This function should be called before any write operation is performed on the database
+     * so that a tracking link is created between observers and its interest tables.
+     *
+     * @see refreshAsync
+     */
+    internal suspend fun sync()
+
+    /**
+     * Refresh subscribed observers asynchronously, invoking [Observer.onInvalidated] on those whose
+     * tables have been invalidated.
+     *
+     * This function should be called after any write operation is performed on the database,
+     * such that tracked tables and its associated observers are notified if invalidated.
+     *
+     * @see sync
+     */
+    internal fun refreshAsync()
+
+    /**
+     * Stops invalidation tracker operations.
+     */
+    internal fun stop()
+
+    /**
+     * An observer that can listen for changes in the database by subscribing to an
+     * [InvalidationTracker].
+     *
+     * @param tables The names of the tables this observer is interested in getting notified if
+     * they are modified.
+     */
+    abstract class Observer(tables: Array<out String>) {
+
+        internal val tables: Array<out String>
+
+        /**
+         * Creates an observer for the given tables and views.
+         *
+         * @param firstTable The name of the table or view.
+         * @param rest       More names of tables or views.
+         */
+        protected constructor(firstTable: String, vararg rest: String)
+
+        /**
+         * Invoked when one of the observed tables is invalidated (changed).
+         *
+         * @param tables A set of invalidated tables. When the observer is interested in multiple
+         * tables, this set can be used to distinguish which of the observed tables were
+         * invalidated. When observing a database view the names of underlying tables will be in
+         * the set instead of the view name.
+         */
+        abstract fun onInvalidated(tables: Set<String>)
+    }
+}
+
+/**
+ * A TRIGGER based implementation of an invalidation tracker.
+ *
+ * Some details on how this tracker works:
+ * * An in-memory table is created with two columns, 'table_id' and 'invalidated' to known which
+ * table has been modified.
+ * * [ObservedTableStates] keeps the 'observed' state of each table helping the tracker know which
+ * tables should be watched (via an installed trigger) based on the number of observers interested.
+ * * Before a write transaction, Room will sync triggers by invoking [InvalidationTracker.sync].
+ * * If in the write transaction a table was modified, the installed trigger will flip the table's
+ * invalidated column in the in-memory table to ON.
+ * * After a write transaction, Room will check the invalidated rows by invoking
+ * [InvalidationTracker.refreshAsync], notifying observers if necessary.
+ */
+internal class TriggerBasedInvalidationTracker(
+    private val database: RoomDatabase,
+    // Table to shadow / content table names
+    private val shadowTablesMap: Map<String, String>,
+    // View to underlying table names
+    private val viewTables: Map<String, Set<String>>,
+    tableNames: Array<out String>
+) {
+    /** Table name (lowercase) to index (id) in [tablesNames], used as a quick lookup map. */
+    private val tableIdLookup: Map<String, Int>
+    /** Table names (lowercase), the index is at which a table is in the array is its 'id'. */
+    private val tablesNames: Array<String>
+
+    private val observerMap: MutableMap<Observer, ObserverWrapper>
+    private val observerMapLock = reentrantLock()
+    private val observedTableStates: ObservedTableStates
+
+    /**
+     * Whether there is a pending [refreshInvalidation] to be done or not. Since a refresh can
+     * be queue to be done asynchronously, this flag is used to control excessive scheduling of
+     * refreshes.
+     */
+    private val pendingRefresh = atomic(false)
+
+    /** Callback to allow or disallow [refreshInvalidation] from proceeding. */
+    internal var onAllowRefresh: () -> Boolean = { true }
+
+    init {
+        tableIdLookup = mutableMapOf()
+        tablesNames = Array(tableNames.size) { id ->
+            val tableName = tableNames[id].lowercase()
+            tableIdLookup[tableName] = id
+            val shadowTableName = shadowTablesMap[tableNames[id]]?.lowercase()
+            shadowTableName ?: tableName
+        }
+
+        // Adjust table id lookup for those tables whose shadow table is another already mapped
+        // table (e.g. external content fts tables).
+        shadowTablesMap.forEach { entry ->
+            val shadowTableName = entry.value.lowercase()
+            if (tableIdLookup.containsKey(shadowTableName)) {
+                val tableName = entry.key.lowercase()
+                tableIdLookup[tableName] = tableIdLookup.getValue(shadowTableName)
+            }
+        }
+
+        observerMap = mutableMapOf()
+        observedTableStates = ObservedTableStates(tablesNames.size)
+    }
+
+    /**
+     * Configure a connection. All connections open by Room should be configured by the tracker
+     * even though the one we really care about is the single write connection.
+     */
+    fun configureConnection(connection: SQLiteConnection) {
+        connection.execSQL("PRAGMA temp_store = MEMORY")
+        connection.execSQL("PRAGMA recursive_triggers = 1")
+        connection.execSQL(CREATE_TRACKING_TABLE_SQL)
+    }
+
+    /**
+     * Add an observer and return true if it was actually added, or false if already added.
+     */
+    internal suspend fun addObserver(observer: Observer): Boolean {
+        val (resolvedTableNames, tableIds) = validateTableNames(observer.tables)
+        val wrapper = ObserverWrapper(
+            observer = observer,
+            tableIds = tableIds,
+            tableNames = resolvedTableNames
+        )
+
+        val currentObserver = observerMapLock.withLock {
+            if (observerMap.containsKey(observer)) {
+                observerMap.getValue(observer)
+            } else {
+                observerMap.put(observer, wrapper)
+            }
+        }
+        val shouldSync = currentObserver == null && observedTableStates.onObserverAdded(tableIds)
+        if (shouldSync) {
+            syncTriggers()
+        }
+        return shouldSync
+    }
+
+    /**
+     * Removes an observer and return true if it was actually removed, or false if it was not found.
+     */
+    internal suspend fun removeObserver(observer: Observer): Boolean {
+        val wrapper = observerMapLock.withLock {
+            observerMap.remove(observer)
+        }
+        val shouldSync = wrapper != null && observedTableStates.onObserverRemoved(wrapper.tableIds)
+        if (shouldSync) {
+            syncTriggers()
+        }
+        return shouldSync
+    }
+
+    /**
+     * Resolves the list of tables and views into unique table names and ids.
+     */
+    internal fun validateTableNames(names: Array<out String>): Pair<Array<String>, IntArray> {
+        val tableNames = resolveViews(names)
+        val tableIds = IntArray(tableNames.size) { i ->
+            val tableName = tableNames[i]
+            tableIdLookup[tableName.lowercase()]
+                ?: throw IllegalArgumentException("There is no table with name $tableName")
+        }
+        return tableNames to tableIds
+    }
+
+    /**
+     * Resolves the list of tables and views into a list of unique tables, i.e. if given a view
+     * then its underlying tables is expanded into the result.
+     */
+    private fun resolveViews(names: Array<out String>): Array<String> {
+        return buildSet {
+            names.forEach { name ->
+                viewTables[name.lowercase()]?.let { addAll(it) } ?: add(name)
+            }
+        }.toTypedArray()
+    }
+
+    /**
+     * Synchronizes database triggers with observed tables.
+     */
+    internal suspend fun syncTriggers() = database.closeBarrier.ifNotClosed {
+        database.useConnection(isReadOnly = false) { connection ->
+            if (connection.inTransaction()) {
+                // Triggers are not synced if the connection is already in a transaction, an
+                // indication that this is a nested transaction and sync is expected to be
+                // invoked before starting a top-level transaction.
+                return@useConnection
+            }
+            connection.withTransaction(SQLiteTransactionType.IMMEDIATE) {
+                observedTableStates.getTablesToSync()?.forEachIndexed { tableId, observeOp ->
+                    when (observeOp) {
+                        ObservedTableStates.ObserveOp.NO_OP -> {}
+                        ObservedTableStates.ObserveOp.ADD ->
+                            startTrackingTable(connection, tableId)
+
+                        ObservedTableStates.ObserveOp.REMOVE ->
+                            stopTrackingTable(connection, tableId)
+                    }
+                }
+            }
+        }
+    }
+
+    private suspend fun startTrackingTable(connection: PooledConnection, tableId: Int) {
+        connection.execSQL("INSERT OR IGNORE INTO $UPDATE_TABLE_NAME VALUES($tableId, 0)")
+        val tableName = tablesNames[tableId]
+        for (trigger in TRIGGERS) {
+            val triggerName = getTriggerName(tableName, trigger)
+            connection.execSQL(
+                "CREATE TEMP TRIGGER IF NOT EXISTS `$triggerName` " +
+                    "AFTER $trigger ON `$tableName` BEGIN " +
+                    "UPDATE $UPDATE_TABLE_NAME SET $INVALIDATED_COLUMN_NAME = 1 " +
+                    "WHERE $TABLE_ID_COLUMN_NAME = $tableId AND $INVALIDATED_COLUMN_NAME = 0; " +
+                    "END"
+            )
+        }
+    }
+
+    private suspend fun stopTrackingTable(connection: PooledConnection, tableId: Int) {
+        val tableName = tablesNames[tableId]
+        for (trigger in TRIGGERS) {
+            val triggerName = getTriggerName(tableName, trigger)
+            connection.execSQL("DROP TRIGGER IF EXISTS `$triggerName`")
+        }
+    }
+
+    /**
+     * Attempts to notify invalidated observers if there is a pending refresh. If there is no
+     * pending refresh (no previous call to [refreshInvalidationAsync] then this function does
+     * nothing.
+     *
+     * This can be useful to accelerate a pending refresh instead of waiting for the coroutine
+     * to launch.
+     */
+    internal suspend fun refreshInvalidation(
+        onRefreshScheduled: () -> Unit = {},
+        onRefreshCompleted: () -> Unit = {},
+    ) {
+        onRefreshScheduled.invoke()
+        try {
+            notifyInvalidatedObservers()
+        } finally {
+            onRefreshCompleted.invoke()
+        }
+    }
+
+    /**
+     * Launches a coroutine to notify invalidated observers.
+     */
+    internal fun refreshInvalidationAsync(
+        onRefreshScheduled: () -> Unit = {},
+        onRefreshCompleted: () -> Unit = {},
+    ) {
+        if (pendingRefresh.compareAndSet(expect = false, update = true)) {
+            onRefreshScheduled.invoke()
+            database.getCoroutineScope().launch {
+                try {
+                    notifyInvalidatedObservers()
+                } finally {
+                    onRefreshCompleted.invoke()
+                }
+            }
+        }
+    }
+
+    private suspend fun notifyInvalidatedObservers() = database.closeBarrier.ifNotClosed {
+        if (!pendingRefresh.compareAndSet(expect = true, update = false)) {
+            // No pending refresh
+            return
+        }
+        if (!onAllowRefresh()) {
+            // Compatibility callback is disallowing a refresh.
+            return
+        }
+        val invalidatedTableIds = database.useConnection(isReadOnly = false) { connection ->
+            if (connection.inTransaction()) {
+                // Skip refresh if connection is already in a transaction, an indication that
+                // this is a nested transaction and refresh is expected to be invoked after
+                // completing a top-level transaction.
+                return@useConnection emptySet()
+            }
+            try {
+                connection.withTransaction(SQLiteTransactionType.IMMEDIATE) {
+                    checkInvalidatedTables(this)
+                }
+            } catch (ex: SQLiteException) {
+                // TODO(b/309990302): We used to log the exception, add the log back.
+                emptySet()
+            }
+        }
+        if (invalidatedTableIds.isNotEmpty()) {
+            notifyInvalidatedTableIds(invalidatedTableIds)
+        }
+    }
+
+    /**
+     * Checks which tables have been invalidated and resets their invalidation state.
+     */
+    private suspend fun checkInvalidatedTables(connection: PooledConnection): Set<Int> {
+        val invalidatedTableIds = connection.usePrepared(SELECT_UPDATED_TABLES_SQL) { statement ->
+            buildSet {
+                while (statement.step()) {
+                    add(statement.getLong(0).toInt())
+                }
+            }
+        }
+        if (invalidatedTableIds.isNotEmpty()) {
+            connection.execSQL(RESET_UPDATED_TABLES_SQL)
+        }
+        return invalidatedTableIds
+    }
+
+    private fun notifyInvalidatedTableIds(tableIds: Set<Int>) {
+        observerMapLock.withLock {
+            observerMap.values.forEach {
+                it.notifyByTableIds(tableIds)
+            }
+        }
+    }
+
+    internal fun notifyInvalidatedTableNames(
+        tableNames: Set<String>,
+        filterPredicate: (Observer) -> Boolean = { true }
+    ) {
+        observerMapLock.withLock {
+            observerMap.values.forEach {
+                if (filterPredicate(it.observer)) {
+                    it.notifyByTableNames(tableNames)
+                }
+            }
+        }
+    }
+
+    internal fun getAllObservers() = observerMap.keys
+
+    internal fun resetSync() {
+        observedTableStates.resetTriggerState()
+    }
+
+    companion object {
+        private val TRIGGERS = arrayOf("INSERT", "UPDATE", "DELETE")
+
+        private const val UPDATE_TABLE_NAME = "room_table_modification_log"
+        private const val TABLE_ID_COLUMN_NAME = "table_id"
+        private const val INVALIDATED_COLUMN_NAME = "invalidated"
+
+        private const val CREATE_TRACKING_TABLE_SQL =
+            "CREATE TEMP TABLE IF NOT EXISTS $UPDATE_TABLE_NAME (" +
+                "$TABLE_ID_COLUMN_NAME INTEGER PRIMARY KEY, " +
+                "$INVALIDATED_COLUMN_NAME INTEGER NOT NULL DEFAULT 0)"
+
+        private const val SELECT_UPDATED_TABLES_SQL =
+            "SELECT * FROM $UPDATE_TABLE_NAME WHERE $INVALIDATED_COLUMN_NAME = 1"
+
+        private const val RESET_UPDATED_TABLES_SQL =
+            "UPDATE $UPDATE_TABLE_NAME SET $INVALIDATED_COLUMN_NAME = 0 " +
+                "WHERE $INVALIDATED_COLUMN_NAME = 1"
+
+        private fun getTriggerName(tableName: String, triggerType: String) =
+            "room_table_modification_trigger_${tableName}_$triggerType"
+    }
+}
+
+/**
+ * Keeps track of which table has to be observed or not due to having one or more observer.
+ *
+ * Call [onObserverAdded] when an observer is added and [onObserverRemoved] when removing one.
+ * To check if a table needs to be tracked or not, call [getTablesToSync].
+ */
+internal class ObservedTableStates(size: Int) {
+
+    private val lock = reentrantLock()
+
+    // The number of observers per table
+    private val tableObserversCount = LongArray(size)
+
+    // The observation state of each table, i.e. true or false if table at ith index should be
+    // observed. These states are only valid if `needsSync` is false.
+    private val tableObservedState = BooleanArray(size)
+
+    private var needsSync = false
+
+    /**
+     * Gets an array of operations to be performed for table at index i from the last time this
+     * function was called and based on the [onObserverAdded] and [onObserverRemoved] invocations
+     * that occurred in-between.
+     */
+    internal fun getTablesToSync(): Array<ObserveOp>? = lock.withLock {
+        if (!needsSync) {
+            return null
+        }
+        needsSync = false
+        Array(tableObserversCount.size) { i ->
+            val newState = tableObserversCount[i] > 0
+            if (newState != tableObservedState[i]) {
+                tableObservedState[i] = newState
+                if (newState) ObserveOp.ADD else ObserveOp.REMOVE
+            } else {
+                ObserveOp.NO_OP
+            }
+        }
+    }
+
+    /**
+     * Notifies that an observer was added and return true if the state of some table changed.
+     */
+    internal fun onObserverAdded(tableIds: IntArray): Boolean = lock.withLock {
+        var shouldSync = false
+        tableIds.forEach { tableId ->
+            val previousCount = tableObserversCount[tableId]
+            tableObserversCount[tableId] = previousCount + 1
+            if (previousCount == 0L) {
+                needsSync = true
+                shouldSync = true
+            }
+        }
+        return shouldSync
+    }
+
+    /**
+     * Notifies that an observer was removed and return true if the state of some table changed.
+     */
+    internal fun onObserverRemoved(tableIds: IntArray): Boolean = lock.withLock {
+        var shouldSync = false
+        tableIds.forEach { tableId ->
+            val previousCount = tableObserversCount[tableId]
+            tableObserversCount[tableId] = previousCount - 1
+            if (previousCount == 1L) {
+                needsSync = true
+                shouldSync = true
+            }
+        }
+        return shouldSync
+    }
+
+    internal fun resetTriggerState() = lock.withLock {
+        tableObservedState.fill(element = false)
+        needsSync = true
+    }
+
+    internal enum class ObserveOp {
+        NO_OP, // Don't change observation / tracking state for a table
+        ADD, // Starting observation / tracking of a table
+        REMOVE // Stop observation / tracking of a table
+    }
+}
+
+/**
+ * Wraps an [Observer] and keeps the table information.
+ *
+ * Internally table ids are used which may change from database to database so the table
+ * related information is kept here rather than in the actual observer.
+ */
+internal class ObserverWrapper(
+    internal val observer: Observer,
+    internal val tableIds: IntArray,
+    private val tableNames: Array<out String>
+) {
+    init {
+        check(tableIds.size == tableNames.size)
+    }
+
+    // Optimization for a single-table observer
+    private val singleTableSet = if (tableNames.isNotEmpty()) setOf(tableNames[0]) else emptySet()
+
+    internal fun notifyByTableIds(invalidatedTablesIds: Set<Int>) {
+        val invalidatedTables = when (tableIds.size) {
+            0 -> emptySet()
+            1 -> if (invalidatedTablesIds.contains(tableIds[0])) singleTableSet else emptySet()
+            else -> buildSet {
+                tableIds.forEachIndexed { id, tableId ->
+                    if (invalidatedTablesIds.contains(tableId)) {
+                        add(tableNames[id])
+                    }
+                }
+            }
+        }
+        if (invalidatedTables.isNotEmpty()) {
+            observer.onInvalidated(invalidatedTables)
+        }
+    }
+
+    internal fun notifyByTableNames(invalidatedTablesNames: Set<String>) {
+        val invalidatedTables = when (tableNames.size) {
+            0 -> emptySet()
+            1 -> if (invalidatedTablesNames.any { it.equals(tableNames[0], ignoreCase = true) }) {
+                singleTableSet
+            } else {
+                emptySet()
+            }
+            else -> buildSet {
+                invalidatedTablesNames.forEach { table ->
+                    for (ourTable in tableNames) {
+                        if (ourTable.equals(table, ignoreCase = true)) {
+                            add(ourTable)
+                            break
+                        }
+                    }
+                }
+            }
+        }
+        if (invalidatedTables.isNotEmpty()) {
+            observer.onInvalidated(invalidatedTables)
+        }
+    }
 }
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/Room.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/Room.kt
index 24c13d7..6035a4b 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/Room.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/Room.kt
@@ -19,4 +19,9 @@
 /**
  * Entry point for building and initializing a [RoomDatabase].
  */
-expect object Room
+expect object Room {
+    /**
+     * The master table name where Room keeps its metadata information.
+     */
+    val MASTER_TABLE_NAME: String
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
index 3cfd90e..74d9ef5 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
@@ -16,9 +16,9 @@
 
 package androidx.room
 
+import androidx.annotation.RestrictTo
 import androidx.room.RoomDatabase.JournalMode.TRUNCATE
 import androidx.room.RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING
-import androidx.room.coroutines.ConnectionPool
 import androidx.room.util.findMigrationPath
 import androidx.room.util.isMigrationRequired
 import androidx.sqlite.SQLiteConnection
@@ -35,10 +35,10 @@
  * Base class for Room's database connection manager, responsible for opening and managing such
  * connections, including performing migrations if necessary and validating schema.
  */
-internal abstract class BaseRoomConnectionManager {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class BaseRoomConnectionManager {
 
     protected abstract val configuration: DatabaseConfiguration
-    protected abstract val connectionPool: ConnectionPool
     protected abstract val openDelegate: RoomOpenDelegate
     protected abstract val callbacks: List<RoomDatabase.Callback>
 
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
index 4c356f0..d959d4b4 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
@@ -19,15 +19,19 @@
 package androidx.room
 
 import androidx.annotation.RestrictTo
+import androidx.room.concurrent.CloseBarrier
 import androidx.room.migration.AutoMigrationSpec
 import androidx.room.migration.Migration
 import androidx.room.util.contains
 import androidx.room.util.isAssignableFrom
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
+import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 import kotlin.reflect.KClass
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
 
 /**
  * Base class for all Room databases. All classes that are annotated with [Database] must
@@ -51,6 +55,22 @@
     val invalidationTracker: InvalidationTracker
 
     /**
+     * A barrier that prevents the database from closing while the [InvalidationTracker] is using
+     * the database asynchronously.
+     *
+     * @return The barrier for [close].
+     */
+    internal val closeBarrier: CloseBarrier
+
+    /**
+     * Called by Room when it is initialized.
+     *
+     * @param configuration The database configuration.
+     * @throws IllegalArgumentException if initialization fails.
+     */
+    internal fun init(configuration: DatabaseConfiguration)
+
+    /**
      * Creates a connection manager to manage database connection. Note that this method
      * is called when the [RoomDatabase] is initialized.
      *
@@ -82,6 +102,8 @@
      */
     protected abstract fun createInvalidationTracker(): InvalidationTracker
 
+    internal fun getCoroutineScope(): CoroutineScope
+
     /**
      * Returns a Set of required [AutoMigrationSpec] classes.
      *
@@ -199,6 +221,19 @@
         fun setDriver(driver: SQLiteDriver): Builder<T>
 
         /**
+         * Sets the [CoroutineContext] that will be used to execute all asynchronous queries and
+         * tasks, such as `Flow` emissions and [InvalidationTracker] notifications.
+         *
+         * If no [CoroutineDispatcher] is present in the [context] then this function will throw
+         * an [IllegalArgumentException]
+         *
+         * @param context The context
+         * @return This [Builder] instance
+         * @throws IllegalArgumentException if the [context] has no [CoroutineDispatcher]
+         */
+        fun setQueryCoroutineContext(context: CoroutineContext): Builder<T>
+
+        /**
          * Adds a [Callback] to this database.
          *
          * @param callback The callback.
@@ -219,7 +254,7 @@
      * A container to hold migrations. It also allows querying its contents to find migrations
      * between two versions.
      */
-    class MigrationContainer {
+    class MigrationContainer() {
         /**
          * Returns the map of available migrations where the key is the start version of the
          * migration, and the value is a map of (end version -> Migration).
@@ -229,12 +264,31 @@
         fun getMigrations(): Map<Int, Map<Int, Migration>>
 
         /**
+         * Adds the given migrations to the list of available migrations. If 2 migrations have the
+         * same start-end versions, the latter migration overrides the previous one.
+         *
+         * @param migrations List of available migrations.
+         */
+        fun addMigrations(migrations: List<Migration>)
+
+        /**
          * Add a [Migration] to the container. If the container already has a migration with the
          * same start-end versions then it will be overwritten.
          *
          * @param migration the migration to add.
          */
-        internal fun addMigration(migration: Migration)
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        fun addMigration(migration: Migration)
+
+        /**
+         * Indicates if the given migration is contained within the [MigrationContainer] based
+         * on its start-end versions.
+         *
+         * @param startVersion Start version of the migration.
+         * @param endVersion End version of the migration
+         * @return True if it contains a migration with the same start-end version, false otherwise.
+         */
+        fun contains(startVersion: Int, endVersion: Int): Boolean
 
         /**
          * Returns a pair corresponding to an entry in the map of available migrations whose key
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
new file mode 100644
index 0000000..7bdb29e
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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.
+ *
+ * Usage is similar to a 'withLock':
+ * ```
+ * closeBarrier.ifNotClosed {
+ *    // Will enter the block if close() has not been called,
+ *    // while also preventing the close() action from occurring.
+ * }
+ * ```
+ * Ideally we would use a read-write mutex, but it does not exist yet,
+ * see https://github.com/Kotlin/kotlinx.coroutines/issues/94.
+ *
+ * @param [closeAction] The action to be performed exactly once and when there are no pending
+ * blockers.
+ */
+internal class CloseBarrier(
+    private val closeAction: () -> Unit
+) : SynchronizedObject() {
+    private val blockers = atomic(0)
+    private val closeInitiated = atomic(false)
+    private val isClosed by closeInitiated
+
+    /**
+     * Blocks the [closeAction] from occurring.
+     *
+     * A call to this function must be balanced with [unblock] after.
+     *
+     * @return `true` if the block is registered and the resource is protected from closing, or
+     * `false` if [close] has been called and the block is not registered.
+     *
+     * @see ifNotClosed
+     */
+    internal fun block(): Boolean = synchronized(this) {
+        if (isClosed) {
+            return false
+        }
+        blockers.incrementAndGet()
+        return true
+    }
+
+    /**
+     * Unblocks the [closeAction] from occurring.
+     *
+     * A call to this function must be balanced with [block] before.
+     *
+     * @see ifNotClosed
+     */
+    internal fun unblock(): Unit = synchronized(this) {
+        blockers.decrementAndGet()
+        check(blockers.value >= 0) { "Unbalanced call to unblock() detected." }
+    }
+
+    /**
+     * Executes the [closeAction] once there are no blockers.
+     *
+     * If there are any pending blockers, it will wait until all blockers are unblocked, and then
+     * execute the [closeAction]. In other words, executes the [closeAction] once no callers of this
+     * object are performing the [ifNotClosed] action or alternatively all callers of [block] have
+     * called their [unblock].
+     */
+    internal fun close() {
+        synchronized(this) {
+            if (!closeInitiated.compareAndSet(expect = false, update = true)) {
+                // already closed, do nothing
+                return
+            }
+        }
+        blockers.loop { count ->
+            if (count == 0) {
+                return closeAction.invoke()
+            }
+        }
+    }
+}
+
+/**
+ * Executes the [action] if [CloseBarrier.close] has not been called on this object.
+ */
+internal inline fun CloseBarrier.ifNotClosed(action: () -> Unit) {
+    if (!block()) return
+    try {
+        action.invoke()
+    } finally {
+        unblock()
+    }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/FlowBuilder.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/FlowBuilder.kt
new file mode 100644
index 0000000..658fcde
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/FlowBuilder.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.
+ */
+
+@file:JvmName("FlowUtil")
+
+package androidx.room.coroutines
+
+import androidx.annotation.RestrictTo
+import androidx.room.InvalidationTracker
+import androidx.room.RoomDatabase
+import androidx.room.util.getCoroutineContext
+import androidx.room.util.internalPerform
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmName
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.launch
+
+// TODO(b/329315924): Migrate to Flow based machinery.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun <R> createFlow(
+    db: RoomDatabase,
+    inTransaction: Boolean,
+    tableNames: Array<String>,
+    block: (SQLiteConnection) -> R
+): Flow<R> = flow {
+    coroutineScope {
+        // Observer channel receives signals from the invalidation tracker to emit queries.
+        val observerChannel = Channel<Unit>(Channel.CONFLATED)
+        val observer = object : InvalidationTracker.Observer(tableNames) {
+            override fun onInvalidated(tables: Set<String>) {
+                observerChannel.trySend(Unit)
+            }
+        }
+        observerChannel.trySend(Unit) // Initial signal to perform first query.
+        val resultChannel = Channel<R>()
+        launch(db.getCoroutineContext(inTransaction).minusKey(Job)) {
+            db.invalidationTracker.subscribe(observer)
+            try {
+                // Iterate until cancelled, transforming observer signals to query results
+                // to be emitted to the flow.
+                for (signal in observerChannel) {
+                    val result = db.internalPerform(true, inTransaction) { connection ->
+                        val rawConnection = (connection as RawConnectionAccessor).rawConnection
+                        block.invoke(rawConnection)
+                    }
+                    resultChannel.send(result)
+                }
+            } finally {
+                db.invalidationTracker.unsubscribe(observer)
+            }
+        }
+
+        emitAll(resultChannel)
+    }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/DBUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/DBUtil.kt
index 1129336..5205e43 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/DBUtil.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/DBUtil.kt
@@ -20,12 +20,15 @@
 package androidx.room.util
 
 import androidx.annotation.RestrictTo
+import androidx.room.PooledConnection
 import androidx.room.RoomDatabase
+import androidx.room.Transactor
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteException
 import androidx.sqlite.SQLiteStatement
 import androidx.sqlite.execSQL
 import androidx.sqlite.use
+import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 
@@ -40,6 +43,39 @@
     block: (SQLiteConnection) -> R
 ): R
 
+internal suspend inline fun <R> RoomDatabase.internalPerform(
+    isReadOnly: Boolean,
+    inTransaction: Boolean,
+    crossinline block: suspend (PooledConnection) -> R
+): R = useConnection(isReadOnly) { transactor ->
+    if (inTransaction) {
+        val type = if (isReadOnly) {
+            Transactor.SQLiteTransactionType.DEFERRED
+        } else {
+            Transactor.SQLiteTransactionType.IMMEDIATE
+        }
+        if (!isReadOnly && !transactor.inTransaction()) {
+            invalidationTracker.sync()
+        }
+        val result = transactor.withTransaction(type) { block.invoke(this) }
+        if (!isReadOnly && !transactor.inTransaction()) {
+            invalidationTracker.refreshAsync()
+        }
+        result
+    } else {
+        block.invoke(transactor)
+    }
+}
+
+/**
+ * Gets the database [CoroutineContext] to perform database operation on utility functions. Prefer
+ * using this function over directly accessing [RoomDatabase.getCoroutineScope] as it has platform
+ * compatibility behaviour.
+ */
+internal expect suspend fun RoomDatabase.getCoroutineContext(
+    inTransaction: Boolean
+): CoroutineContext
+
 /**
  * Utility function to wrap a suspend block in Room's transaction coroutine.
  *
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/MigrationUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/MigrationUtil.kt
index 85119cd..d4f233e 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/MigrationUtil.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/MigrationUtil.kt
@@ -60,7 +60,7 @@
  * @param endVersion End version of the migration
  * @return True if it contains a migration with the same start-end version, false otherwise.
  */
-@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // On purpose and only in Android source set.
+@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // On purpose in non-common platforms source sets.
 internal fun MigrationContainer.contains(startVersion: Int, endVersion: Int): Boolean {
     val migrations = getMigrations()
     if (migrations.containsKey(startVersion)) {
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
new file mode 100644
index 0000000..1b35386
--- /dev/null
+++ b/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.concurrent
+
+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
+import kotlinx.coroutines.newFixedThreadPoolContext
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.yield
+
+class CloseBarrierTest {
+
+    @Test
+    @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
+    fun oneBlocker() = runTest {
+        val actionPerformed = atomic(false)
+        val closeBarrier = CloseBarrier {
+            assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+        }
+        val jobLaunched = Mutex(locked = true)
+
+        // block the barrier
+        assertThat(closeBarrier.block()).isTrue()
+
+        // launch a close action, expect it to wait since there is one blocker
+        val closeJob = launch(newFixedThreadPoolContext(1, "CloseThread")) {
+            jobLaunched.unlock()
+            closeBarrier.close()
+        }
+
+        // yield for launch and verify the close action has not been performed
+        yield()
+        jobLaunched.withLock {
+            assertThat(actionPerformed.value).isFalse()
+        }
+
+        // unblock the barrier, close job should complete
+        closeBarrier.unblock()
+        closeJob.join()
+
+        // verify action was performed
+        assertThat(actionPerformed.value).isTrue()
+
+        // verify a new block is not granted since the barrier is already close
+        assertThat(closeBarrier.block()).isFalse()
+    }
+
+    @Test
+    fun noBlockers() = runTest {
+        val actionPerformed = atomic(false)
+        val closeBarrier = CloseBarrier {
+            assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+        }
+
+        // Validate close action is performed immediately if there are no blockers
+        closeBarrier.close()
+
+        assertThat(actionPerformed.value).isTrue()
+    }
+
+    @Test
+    fun unbalancedBlocker() = runTest {
+        val closeBarrier = CloseBarrier {}
+        assertThrows<IllegalStateException> {
+            closeBarrier.unblock()
+        }.hasMessageThat().isEqualTo("Unbalanced call to unblock() detected.")
+    }
+
+    @Test
+    @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
+    fun noStarvation() = runTest {
+        val actionPerformed = atomic(false)
+        val closeBarrier = CloseBarrier {
+            assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+        }
+        val jobLaunched = Mutex(locked = true)
+
+        // launch a heavy blocker, it should not starve the close action
+        val blockerJob = launch(newFixedThreadPoolContext(1, "BlockerThread")) {
+            jobLaunched.unlock()
+            while (true) {
+                if (closeBarrier.block()) {
+                    closeBarrier.unblock()
+                } else {
+                    break
+                }
+            }
+        }
+
+        // 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()
+        }
+
+        // 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()
+    }
+}
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/Room.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/Room.jvm.kt
index 1ab569b..4626508 100644
--- a/room/room-runtime/src/jvmMain/kotlin/androidx/room/Room.jvm.kt
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/Room.jvm.kt
@@ -24,6 +24,11 @@
 actual object Room {
 
     /**
+     * The master table name where Room keeps its metadata information.
+     */
+    actual const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME
+
+    /**
      * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
      * database disappears when the process is killed. Once a database is built, you should keep a
      * reference to it and re-use it.
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/DatabaseConfiguration.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/DatabaseConfiguration.jvmNative.kt
index 5c298ca..0d6c83d 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/DatabaseConfiguration.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/DatabaseConfiguration.jvmNative.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.migration.AutoMigrationSpec
 import androidx.sqlite.SQLiteDriver
+import kotlin.coroutines.CoroutineContext
 
 /**
  * Configuration class for a [RoomDatabase].
@@ -34,5 +35,6 @@
     internal actual val migrationNotRequiredFrom: Set<Int>?,
     actual val typeConverters: List<Any>,
     actual val autoMigrationSpecs: List<AutoMigrationSpec>,
-    actual val sqliteDriver: SQLiteDriver?
+    actual val sqliteDriver: SQLiteDriver?,
+    actual val queryCoroutineContext: CoroutineContext?,
 )
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
index 98e4dfe..4ec00aa 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
@@ -17,11 +17,18 @@
 package androidx.room
 
 import androidx.annotation.RestrictTo
+import androidx.room.InvalidationTracker.Observer
 import androidx.sqlite.SQLiteConnection
 
 /**
- * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * The invalidation tracker keeps track of tables modified by queries and notifies its subscribed
  * [Observer]s about such modifications.
+ *
+ * [Observer]s contain one or more tables and are added to the tracker via [subscribe]. Once
+ * an observer is subscribed, if a database operation changes one of the tables the observer is
+ * subscribed to, then such table is considered 'invalidated' and [Observer.onInvalidated] will
+ * be invoked on the observer. If an observer is no longer interested in tracking modifications
+ * it can be removed via [unsubscribe].
  */
 actual class InvalidationTracker
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@@ -31,9 +38,103 @@
     viewTables: Map<String, Set<String>>,
     vararg tableNames: String
 ) {
+    private val implementation =
+        TriggerBasedInvalidationTracker(database, shadowTablesMap, viewTables, tableNames)
+
     /**
      * Internal method to initialize table tracking. Invoked by generated code.
      */
     internal actual fun internalInit(connection: SQLiteConnection) {
+        implementation.configureConnection(connection)
+    }
+
+    /**
+     * Subscribes the given [observer] with the tracker such that it is notified if any table it
+     * is interested on changes.
+     *
+     * If the observer is already subscribed, then this function does nothing.
+     *
+     * @param observer The observer that will listen for database changes.
+     * @throws IllegalArgumentException if one of the tables in the observer does not exist.
+     */
+    // TODO(b/329315924): Replace with Flow based API
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun subscribe(observer: Observer) {
+        implementation.addObserver(observer)
+    }
+
+    /**
+     * Unsubscribes the given [observer] from the tracker.
+     *
+     * If the observer was never subscribed in the first place, then this function does nothing.
+     *
+     * @param observer The observer to remove.
+     */
+    // TODO(b/329315924): Replace with Flow based API
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun unsubscribe(observer: Observer) {
+        implementation.removeObserver(observer)
+    }
+
+    /**
+     * Synchronize subscribed observers with their tables.
+     *
+     * This function should be called before any write operation is performed on the database
+     * so that a tracking link is created between observers and its interest tables.
+     *
+     * @see refreshAsync
+     */
+    internal actual suspend fun sync() {
+        implementation.syncTriggers()
+    }
+
+    /**
+     * Refresh subscribed observers asynchronously, invoking [Observer.onInvalidated] on those whose
+     * tables have been invalidated.
+     *
+     * This function should be called after any write operation is performed on the database,
+     * such that tracked tables and its associated observers are notified if invalidated.
+     *
+     * @see sync
+     */
+    internal actual fun refreshAsync() {
+        implementation.refreshInvalidationAsync()
+    }
+
+    /**
+     * Stops invalidation tracker operations.
+     */
+    actual fun stop() {}
+
+    /**
+     * An observer that can listen for changes in the database by subscribing to an
+     * [InvalidationTracker].
+     *
+     * @param tables The names of the tables this observer is interested in getting notified if
+     * they are modified.
+     */
+    actual abstract class Observer actual constructor(
+        internal actual val tables: Array<out String>
+    ) {
+        /**
+         * Creates an observer for the given tables and views.
+         *
+         * @param firstTable The name of the table or view.
+         * @param rest       More names of tables or views.
+         */
+        protected actual constructor(
+            firstTable: String,
+            vararg rest: String
+        ) : this(arrayOf(firstTable, *rest))
+
+        /**
+         * Invoked when one of the observed tables is invalidated (changed).
+         *
+         * @param tables A set of invalidated tables. When the observer is interested in multiple
+         * tables, this set can be used to distinguish which of the observed tables were
+         * invalidated. When observing a database view the names of underlying tables will be in
+         * the set instead of the view name.
+         */
+        actual abstract fun onInvalidated(tables: Set<String>)
     }
 }
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomConnectionManager.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomConnectionManager.jvmNative.kt
index c42c280..2787fbfe 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomConnectionManager.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomConnectionManager.jvmNative.kt
@@ -28,7 +28,7 @@
     override val callbacks: List<RoomDatabase.Callback>,
 ) : BaseRoomConnectionManager() {
 
-    override val connectionPool: ConnectionPool =
+    private val connectionPool: ConnectionPool =
         if (configuration.name == null) {
             // An in-memory database must use a single connection pool.
             newSingleConnectionPool(
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomDatabase.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomDatabase.jvmNative.kt
index a111087..d4cd1f5 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomDatabase.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomDatabase.jvmNative.kt
@@ -20,13 +20,24 @@
 package androidx.room
 
 import androidx.annotation.RestrictTo
+import androidx.room.concurrent.CloseBarrier
 import androidx.room.migration.AutoMigrationSpec
 import androidx.room.migration.Migration
+import androidx.room.util.contains as containsCommon
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 import kotlin.reflect.KClass
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
 
 /**
  * Base class for all Room databases. All classes that are annotated with [Database] must
@@ -40,6 +51,7 @@
 actual abstract class RoomDatabase {
 
     private lateinit var connectionManager: RoomConnectionManager
+    private lateinit var coroutineScope: CoroutineScope
 
     private val typeConverters: MutableMap<KClass<*>, Any> = mutableMapOf()
 
@@ -51,7 +63,18 @@
      *
      * @return The invalidation tracker for the database.
      */
-    actual val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+    actual val invalidationTracker: InvalidationTracker
+        get() = internalTracker
+
+    private lateinit var internalTracker: InvalidationTracker
+
+    /**
+     * A barrier that prevents the database from closing while the [InvalidationTracker] is using
+     * the database asynchronously.
+     *
+     * @return The barrier for [close].
+     */
+    internal actual val closeBarrier = CloseBarrier(::onClosed)
 
     /**
      * Called by Room when it is initialized.
@@ -59,8 +82,13 @@
      * @param configuration The database configuration.
      * @throws IllegalArgumentException if initialization fails.
      */
-    internal fun init(configuration: DatabaseConfiguration) {
+    internal actual fun init(configuration: DatabaseConfiguration) {
         connectionManager = createConnectionManager(configuration)
+        internalTracker = createInvalidationTracker()
+        val parentJob = checkNotNull(configuration.queryCoroutineContext)[Job]
+        coroutineScope = CoroutineScope(
+            configuration.queryCoroutineContext + SupervisorJob(parentJob)
+        )
         validateAutoMigrations(configuration)
         validateTypeConverters(configuration)
     }
@@ -104,6 +132,10 @@
      */
     protected actual abstract fun createInvalidationTracker(): InvalidationTracker
 
+    internal actual fun getCoroutineScope(): CoroutineScope {
+        return coroutineScope
+    }
+
     /**
      * Returns a Set of required [AutoMigrationSpec] classes.
      *
@@ -199,6 +231,12 @@
      * Once a [RoomDatabase] is closed it should no longer be used.
      */
     actual fun close() {
+        closeBarrier.close()
+    }
+
+    private fun onClosed() {
+        coroutineScope.cancel()
+        invalidationTracker.stop()
         connectionManager.close()
     }
 
@@ -246,6 +284,7 @@
     ) {
 
         private var driver: SQLiteDriver? = null
+        private var queryCoroutineContext: CoroutineContext? = null
         private val callbacks = mutableListOf<Callback>()
 
         /**
@@ -261,6 +300,26 @@
         }
 
         /**
+         * Sets the [CoroutineContext] that will be used to execute all asynchronous queries and
+         * tasks, such as `Flow` emissions and [InvalidationTracker] notifications.
+         *
+         * If no [CoroutineDispatcher] is present in the [context] then this function will throw
+         * an [IllegalArgumentException]
+         *
+         * If no context is provided, then Room will default to `Dispatchers.IO`.
+         *
+         * @param context The context
+         * @return This [Builder] instance
+         * @throws IllegalArgumentException if the [context] has no [CoroutineDispatcher]
+         */
+        actual fun setQueryCoroutineContext(context: CoroutineContext) = apply {
+            require(context[ContinuationInterceptor] != null) {
+                "It is required that the coroutine context contain a dispatcher."
+            }
+            this.queryCoroutineContext = context
+        }
+
+        /**
          * Adds a [Callback] to this database.
          *
          * @param callback The callback.
@@ -290,7 +349,8 @@
                 migrationNotRequiredFrom = null,
                 typeConverters = emptyList(),
                 autoMigrationSpecs = emptyList(),
-                sqliteDriver = driver
+                sqliteDriver = driver,
+                queryCoroutineContext = queryCoroutineContext ?: Dispatchers.IO,
             )
             val db = factory.invoke()
             db.init(configuration)
@@ -316,12 +376,23 @@
         }
 
         /**
+         * Adds the given migrations to the list of available migrations. If 2 migrations have the
+         * same start-end versions, the latter migration overrides the previous one.
+         *
+         * @param migrations List of available migrations.
+         */
+        actual fun addMigrations(migrations: List<Migration>) {
+            migrations.forEach(::addMigration)
+        }
+
+        /**
          * Add a [Migration] to the container. If the container already has a migration with the
          * same start-end versions then it will be overwritten.
          *
          * @param migration the migration to add.
          */
-        internal actual fun addMigration(migration: Migration) {
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        actual fun addMigration(migration: Migration) {
             val start = migration.startVersion
             val end = migration.endVersion
             val targetMap = migrations.getOrPut(start) { mutableMapOf() }
@@ -329,6 +400,18 @@
         }
 
         /**
+         * Indicates if the given migration is contained within the [MigrationContainer] based
+         * on its start-end versions.
+         *
+         * @param startVersion Start version of the migration.
+         * @param endVersion End version of the migration
+         * @return True if it contains a migration with the same start-end version, false otherwise.
+         */
+        actual fun contains(startVersion: Int, endVersion: Int): Boolean {
+            return this.containsCommon(startVersion, endVersion)
+        }
+
+        /**
          * Returns a pair corresponding to an entry in the map of available migrations whose key
          * is [migrationStart] and its sorted keys in ascending order.
          */
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/DBUtil.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/DBUtil.jvmNative.kt
index ee0158b..84418f2 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/DBUtil.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/DBUtil.jvmNative.kt
@@ -20,13 +20,13 @@
 package androidx.room.util
 
 import androidx.annotation.RestrictTo
-import androidx.room.PooledConnection
 import androidx.room.RoomDatabase
-import androidx.room.Transactor
 import androidx.room.coroutines.RawConnectionAccessor
 import androidx.sqlite.SQLiteConnection
+import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
+import kotlinx.coroutines.withContext
 
 /**
  * Performs a database operation.
@@ -37,12 +37,23 @@
     isReadOnly: Boolean,
     inTransaction: Boolean,
     block: (SQLiteConnection) -> R
-): R = db.internalPerform(isReadOnly, inTransaction) { connection ->
-    val rawConnection = (connection as RawConnectionAccessor).rawConnection
-    block.invoke(rawConnection)
+): R = withContext(db.getCoroutineContext(inTransaction)) {
+    db.internalPerform(isReadOnly, inTransaction) { connection ->
+        val rawConnection = (connection as RawConnectionAccessor).rawConnection
+        block.invoke(rawConnection)
+    }
 }
 
 /**
+ * Gets the database [CoroutineContext] to perform database operation on utility functions. Prefer
+ * using this function over directly accessing [RoomDatabase.getCoroutineScope] as it has platform
+ * compatibility behaviour.
+ */
+internal actual suspend fun RoomDatabase.getCoroutineContext(
+    inTransaction: Boolean
+): CoroutineContext = getCoroutineScope().coroutineContext
+
+/**
  * Utility function to wrap a suspend block in Room's transaction coroutine.
  *
  * This function should only be invoked from generated code and is needed to support `@Transaction`
@@ -52,24 +63,8 @@
 actual suspend fun <R> performInTransactionSuspending(
     db: RoomDatabase,
     block: suspend () -> R
-): R = db.internalPerform(isReadOnly = false, inTransaction = true) {
-    block.invoke()
-}
-
-private suspend inline fun <R> RoomDatabase.internalPerform(
-    isReadOnly: Boolean,
-    inTransaction: Boolean,
-    crossinline block: suspend (PooledConnection) -> R
-): R = useConnection(isReadOnly) { transactor ->
-    if (inTransaction) {
-        val type = if (isReadOnly) {
-            Transactor.SQLiteTransactionType.DEFERRED
-        } else {
-            Transactor.SQLiteTransactionType.IMMEDIATE
-        }
-        // TODO(b/309990302): Notify Invalidation Tracker before and after transaction block.
-        transactor.withTransaction(type) { block.invoke(this) }
-    } else {
-        block.invoke(transactor)
+): R = withContext(db.getCoroutineContext(inTransaction = true)) {
+    db.internalPerform(isReadOnly = false, inTransaction = true) {
+        block.invoke()
     }
 }
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/Room.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/Room.native.kt
index e133d30..319dbea 100644
--- a/room/room-runtime/src/nativeMain/kotlin/androidx/room/Room.native.kt
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/Room.native.kt
@@ -22,6 +22,11 @@
 actual object Room {
 
     /**
+     * The master table name where Room keeps its metadata information.
+     */
+    actual const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME
+
+    /**
      * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
      * database disappears when the process is killed. Once a database is built, you should keep a
      * reference to it and re-use it.
diff --git a/room/room-testing/api/current.txt b/room/room-testing/api/current.txt
index 482f988..becc3e0 100644
--- a/room/room-testing/api/current.txt
+++ b/room/room-testing/api/current.txt
@@ -2,6 +2,7 @@
 package androidx.room.testing {
 
   public class MigrationTestHelper extends org.junit.rules.TestWatcher {
+    ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, androidx.sqlite.SQLiteDriver driver, kotlin.reflect.KClass<? extends androidx.room.RoomDatabase> databaseClass, optional kotlin.jvm.functions.Function0<? extends androidx.room.RoomDatabase> databaseFactory, optional java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
     ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass);
     ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs);
     ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
@@ -9,7 +10,9 @@
     ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
     method public void closeWhenFinished(androidx.room.RoomDatabase db);
     method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public final androidx.sqlite.SQLiteConnection createDatabase(int version);
     method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public androidx.sqlite.db.SupportSQLiteDatabase createDatabase(String name, int version) throws java.io.IOException;
+    method public final androidx.sqlite.SQLiteConnection runMigrationsAndValidate(int version, java.util.List<? extends androidx.room.migration.Migration> migrations);
     method public androidx.sqlite.db.SupportSQLiteDatabase runMigrationsAndValidate(String name, int version, boolean validateDroppedTables, androidx.room.migration.Migration... migrations);
   }
 
diff --git a/room/room-testing/api/restricted_current.txt b/room/room-testing/api/restricted_current.txt
index 482f988..becc3e0 100644
--- a/room/room-testing/api/restricted_current.txt
+++ b/room/room-testing/api/restricted_current.txt
@@ -2,6 +2,7 @@
 package androidx.room.testing {
 
   public class MigrationTestHelper extends org.junit.rules.TestWatcher {
+    ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, androidx.sqlite.SQLiteDriver driver, kotlin.reflect.KClass<? extends androidx.room.RoomDatabase> databaseClass, optional kotlin.jvm.functions.Function0<? extends androidx.room.RoomDatabase> databaseFactory, optional java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
     ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass);
     ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs);
     ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
@@ -9,7 +10,9 @@
     ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
     method public void closeWhenFinished(androidx.room.RoomDatabase db);
     method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public final androidx.sqlite.SQLiteConnection createDatabase(int version);
     method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public androidx.sqlite.db.SupportSQLiteDatabase createDatabase(String name, int version) throws java.io.IOException;
+    method public final androidx.sqlite.SQLiteConnection runMigrationsAndValidate(int version, java.util.List<? extends androidx.room.migration.Migration> migrations);
     method public androidx.sqlite.db.SupportSQLiteDatabase runMigrationsAndValidate(String name, int version, boolean validateDroppedTables, androidx.room.migration.Migration... migrations);
   }
 
diff --git a/room/room-testing/build.gradle b/room/room-testing/build.gradle
index 037fbc6..aa35ba0 100644
--- a/room/room-testing/build.gradle
+++ b/room/room-testing/build.gradle
@@ -46,8 +46,8 @@
                 api(libs.kotlinStdlib)
                 api(project(":room:room-common"))
                 api(project(":room:room-runtime"))
-                api(project(":room:room-migration"))
                 api(project(":sqlite:sqlite"))
+                implementation(project(":room:room-migration"))
             }
         }
         commonTest {
@@ -58,6 +58,9 @@
         }
         jvmMain {
             dependsOn(commonMain)
+            dependencies {
+                api(libs.junit)
+            }
         }
         androidMain {
             dependsOn(commonMain)
@@ -69,6 +72,9 @@
         }
         nativeMain {
             dependsOn(commonMain)
+            dependencies {
+                implementation(libs.okio)
+            }
         }
         targets.all { target ->
             if (target.platformType == KotlinPlatformType.native) {
diff --git a/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt b/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
index 0327a00..2afe271 100644
--- a/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
+++ b/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
@@ -18,83 +18,80 @@
 
 import android.app.Instrumentation
 import android.content.Context
-import android.util.Log
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.room.DatabaseConfiguration
-import androidx.room.Room
+import androidx.room.InvalidationTracker
 import androidx.room.RoomDatabase
+import androidx.room.RoomOpenDelegate
+import androidx.room.driver.SupportSQLiteConnection
+import androidx.room.driver.SupportSQLiteDriver
 import androidx.room.migration.AutoMigrationSpec
 import androidx.room.migration.Migration
-import androidx.room.migration.bundle.DatabaseBundle
-import androidx.room.migration.bundle.FtsEntityBundle
 import androidx.room.migration.bundle.SchemaBundle
-import androidx.room.migration.bundle.SchemaBundle.Companion.deserialize
-import androidx.room.util.FtsTableInfo
-import androidx.room.util.TableInfo
-import androidx.room.util.ViewInfo
 import androidx.room.util.findAndInstantiateDatabaseImpl
-import androidx.room.util.useCursor
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteDriver
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.db.SupportSQLiteOpenHelper
 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
-import java.io.File
 import java.io.FileNotFoundException
 import java.io.IOException
 import java.lang.ref.WeakReference
 import kotlin.reflect.KClass
+import kotlin.reflect.cast
 import org.junit.rules.TestWatcher
 import org.junit.runner.Description
 
 /**
- * A class that can be used in your Instrumentation tests that can create the database in an
- * older schema.
+ * A class that can help test and verify database creation and migration at different versions with
+ * different schemas in Instrumentation tests.
  *
- * You must copy the schema json files (created by passing `room.schemaLocation` argument
- * into the annotation processor) into your test assets and pass in the path for that folder into
- * the constructor. This class will read the folder and extract the schemas from there.
- *
+ * The helper relies on exported schemas so [androidx.room.Database.exportSchema] should
+ * be enabled. Schema location should be configured via Room's Gradle Plugin (id 'androidx.room'):
+ * ```
+ * room {
+ *   schemaDirectory("$projectDir/schemas")
+ * }
+ * ```
+ * The schema files must also be copied into the test assets in order for the helper to open them
+ * during the instrumentation test:
  * ```
  * android {
- *     defaultConfig {
- *         javaCompileOptions {
- *             annotationProcessorOptions {
- *                 arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
- *             }
- *         }
- *     }
  *     sourceSets {
  *         androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
  *     }
  * }
  * ```
+ * See the various constructors documentation for possible configurations.
+ *
+ * See also [Room's Test Migrations Documentation](https://developer.android.com/training/data-storage/room/migrating-db-versions#test)
  */
-open class MigrationTestHelper : TestWatcher {
-    private val assetsFolder: String
-    private val openFactory: SupportSQLiteOpenHelper.Factory
-    private val managedDatabases = mutableListOf<WeakReference<SupportSQLiteDatabase>>()
+actual open class MigrationTestHelper : TestWatcher {
+    private val delegate: AndroidMigrationTestHelper
+
+    private val managedSupportDatabases = mutableListOf<WeakReference<SupportSQLiteDatabase>>()
     private val managedRoomDatabases = mutableListOf<WeakReference<RoomDatabase>>()
+
     private var testStarted = false
-    private val instrumentation: Instrumentation
-    private val specs: List<AutoMigrationSpec>
-    private val databaseClass: Class<out RoomDatabase>?
-    internal lateinit var databaseConfiguration: DatabaseConfiguration
 
     /**
-     * Creates a new migration helper. It uses the Instrumentation context to load the schema
+     * Creates a new migration helper. It uses the [instrumentation] context to load the schema
      * (falls back to the app resources) and the target context to create the database.
      *
+     * When the [MigrationTestHelper] is created with this constructor configuration then only
+     * [createDatabase] and [runMigrationsAndValidate] that return [SupportSQLiteDatabase] can
+     * be used.
+     *
      * @param instrumentation The instrumentation instance.
      * @param assetsFolder    The asset folder in the assets directory.
      * @param openFactory factory for creating an [SupportSQLiteOpenHelper]
      */
     @Deprecated(
         """
-            Cannot be used to run migration tests involving [AutoMigration].
-            To test [AutoMigration], you must use [MigrationTestHelper(Instrumentation, Class, List,
-            SupportSQLiteOpenHelper.Factory)] for tests containing a
-            [androidx.room.ProvidedAutoMigrationSpec], or use
-            [MigrationTestHelper(Instrumentation, Class, List)] otherwise.
-      """
+        Cannot be used to run migration tests involving auto migrations.
+        To test an auto migrations, you must use the constructors that receives the database
+        class as parameter.
+        """
     )
     @JvmOverloads
     constructor(
@@ -102,20 +99,22 @@
         assetsFolder: String,
         openFactory: SupportSQLiteOpenHelper.Factory = FrameworkSQLiteOpenHelperFactory()
     ) {
-        this.instrumentation = instrumentation
-        this.assetsFolder = assetsFolder
-        this.openFactory = openFactory
-        databaseClass = null
-        specs = mutableListOf()
+        this.delegate = SupportSQLiteMigrationTestHelper(
+            instrumentation = instrumentation,
+            assetsFolder = assetsFolder,
+            databaseClass = null,
+            openFactory = openFactory,
+            autoMigrationSpecs = emptyList()
+        )
     }
 
     /**
-     * Creates a new migration helper. It uses the Instrumentation context to load the schema
+     * Creates a new migration helper. It uses the [instrumentation] context to load the schema
      * (falls back to the app resources) and the target context to create the database.
      *
-     * An instance of a class annotated with [androidx.room.ProvidedAutoMigrationSpec] has
-     * to be provided to Room using this constructor. MigrationTestHelper will map auto migration
-     * spec classes to their provided instances before running and validating the Migrations.
+     * When the [MigrationTestHelper] is created with this constructor configuration then only
+     * [createDatabase] and [runMigrationsAndValidate] that return [SupportSQLiteDatabase] can
+     * be used.
      *
      * @param instrumentation The instrumentation instance.
      * @param databaseClass   The Database class to be tested.
@@ -124,22 +123,28 @@
         instrumentation: Instrumentation,
         databaseClass: Class<out RoomDatabase>
     ) : this(
-        instrumentation, databaseClass, emptyList(), FrameworkSQLiteOpenHelperFactory()
+        instrumentation = instrumentation,
+        databaseClass = databaseClass,
+        specs = emptyList(),
+        openFactory = FrameworkSQLiteOpenHelperFactory()
     )
 
     /**
-     * Creates a new migration helper. It uses the Instrumentation context to load the schema
+     * Creates a new migration helper. It uses the [instrumentation] context to load the schema
      * (falls back to the app resources) and the target context to create the database.
      *
+     * Instances of classes annotated with [androidx.room.ProvidedAutoMigrationSpec] have
+     * provided using this constructor. [MigrationTestHelper] will map auto migration
+     * spec classes to their provided instances before running and validating the migrations.
      *
-     * An instance of a class annotated with [androidx.room.ProvidedAutoMigrationSpec] has
-     * to be provided to Room using this constructor. MigrationTestHelper will map auto migration
-     * spec classes to their provided instances before running and validating the Migrations.
+     * When the [MigrationTestHelper] is created with this constructor configuration then only
+     * [createDatabase] and [runMigrationsAndValidate] that return [SupportSQLiteDatabase] can
+     * be used.
      *
      * @param instrumentation The instrumentation instance.
      * @param databaseClass   The Database class to be tested.
      * @param specs           The list of available auto migration specs that will be provided to
-     * Room at runtime.
+     *                        the RoomDatabase at runtime.
      * @param openFactory factory for creating an [SupportSQLiteOpenHelper]
      */
     @JvmOverloads
@@ -149,17 +154,64 @@
         specs: List<AutoMigrationSpec>,
         openFactory: SupportSQLiteOpenHelper.Factory = FrameworkSQLiteOpenHelperFactory()
     ) {
-        this.assetsFolder = checkNotNull(databaseClass.canonicalName).let {
+        val assetsFolder = checkNotNull(databaseClass.canonicalName).let {
             if (it.endsWith("/")) {
-                it.substring(0, databaseClass.canonicalName!!.length - 1)
+                it.substring(0, it.length - 1)
             } else {
                 it
             }
         }
-        this.instrumentation = instrumentation
-        this.openFactory = openFactory
-        this.databaseClass = databaseClass
-        this.specs = specs
+        this.delegate = SupportSQLiteMigrationTestHelper(
+            instrumentation = instrumentation,
+            assetsFolder = assetsFolder,
+            databaseClass = databaseClass,
+            openFactory = openFactory,
+            autoMigrationSpecs = specs
+        )
+    }
+
+    /**
+     * Creates a new migration helper. It uses the [instrumentation] context to load the schema
+     * (falls back to the app resources) and the target context to create the database.
+     *
+     * When the [MigrationTestHelper] is created with this constructor configuration then only
+     * [createDatabase] and [runMigrationsAndValidate] that return [SQLiteConnection] can
+     * be used.
+     *
+     * @param instrumentation The instrumentation instance.
+     * @param driver A driver that opens connection to a file database. A driver that opens connections
+     * to an in-memory database would be meaningless.
+     * @param databaseClass The [androidx.room.Database] annotated class.
+     * @param databaseFactory An optional factory function to create an instance of the
+     * [databaseClass]. Should be the same factory used when building the database via
+     * [androidx.room.Room.databaseBuilder].
+     * @param autoMigrationSpecs The list of [androidx.room.ProvidedAutoMigrationSpec] instances
+     * for [androidx.room.AutoMigration]s that require them.
+     */
+    constructor(
+        instrumentation: Instrumentation,
+        driver: SQLiteDriver,
+        databaseClass: KClass<out RoomDatabase>,
+        databaseFactory: () -> RoomDatabase = {
+            findAndInstantiateDatabaseImpl(databaseClass.java)
+        },
+        autoMigrationSpecs: List<AutoMigrationSpec> = emptyList()
+    ) {
+        val assetsFolder = checkNotNull(databaseClass.qualifiedName).let {
+            if (it.endsWith("/")) {
+                it.substring(0, it.length - 1)
+            } else {
+                it
+            }
+        }
+        this.delegate = SQLiteDriverMigrationTestHelper(
+            instrumentation = instrumentation,
+            assetsFolder = assetsFolder,
+            driver = driver,
+            databaseClass = databaseClass,
+            databaseFactory = databaseFactory,
+            autoMigrationSpecs = autoMigrationSpecs
+        )
     }
 
     override fun starting(description: Description?) {
@@ -169,6 +221,7 @@
 
     /**
      * Creates the database in the given version.
+     *
      * If the database file already exists, it tries to delete it first. If delete fails, throws
      * an exception.
      *
@@ -178,50 +231,11 @@
      */
     @Throws(IOException::class)
     open fun createDatabase(name: String, version: Int): SupportSQLiteDatabase {
-        val dbPath: File = instrumentation.targetContext.getDatabasePath(name)
-        if (dbPath.exists()) {
-            Log.d(TAG, "deleting database file $name")
-            check(dbPath.delete()) {
-                "There is a database file and I could not delete" +
-                    " it. Make sure you don't have any open connections to that database" +
-                    " before calling this method."
-            }
+        check(delegate is SupportSQLiteMigrationTestHelper) {
+            "MigrationTestHelper functionality returning a SupportSQLiteDatabase is not possible " +
+                "because a SQLiteDriver was provided during configuration."
         }
-        val schemaBundle = loadSchema(version)
-        val container: RoomDatabase.MigrationContainer = RoomDatabase.MigrationContainer()
-        val configuration = DatabaseConfiguration(
-            context = instrumentation.targetContext,
-            name = name,
-            sqliteOpenHelperFactory = openFactory,
-            migrationContainer = container,
-            callbacks = null,
-            allowMainThreadQueries = true,
-            journalMode = RoomDatabase.JournalMode.TRUNCATE,
-            queryExecutor = ArchTaskExecutor.getIOThreadExecutor(),
-            transactionExecutor = ArchTaskExecutor.getIOThreadExecutor(),
-            multiInstanceInvalidationServiceIntent = null,
-            requireMigration = true,
-            allowDestructiveMigrationOnDowngrade = false,
-            migrationNotRequiredFrom = emptySet(),
-            copyFromAssetPath = null,
-            copyFromFile = null,
-            copyFromInputStream = null,
-            prepackagedDatabaseCallback = null,
-            typeConverters = emptyList(),
-            autoMigrationSpecs = emptyList(),
-            allowDestructiveMigrationForAllTables = false,
-            sqliteDriver = null
-        )
-        @Suppress("DEPRECATION") // Due to RoomOpenHelper
-        val roomOpenHelper = androidx.room.RoomOpenHelper(
-            configuration = configuration,
-            delegate = CreatingDelegate(schemaBundle.database),
-            identityHash = schemaBundle.database.identityHash,
-            // we pass the same hash twice since an old schema does not necessarily have
-            // a legacy hash and we would not even persist it.
-            legacyHash = schemaBundle.database.identityHash
-        )
-        return openDatabase(name, roomOpenHelper)
+        return delegate.createDatabase(name, version)
     }
 
     /**
@@ -243,7 +257,7 @@
      * @param validateDroppedTables If set to true, validation will fail if the database has
      * unknown tables.
      * @param migrations            The list of available migrations.
-     * @throws IllegalArgumentException If the schema validation fails.
+     * @throws IllegalStateException If the schema validation fails.
      */
     open fun runMigrationsAndValidate(
         name: String,
@@ -251,152 +265,75 @@
         validateDroppedTables: Boolean,
         vararg migrations: Migration
     ): SupportSQLiteDatabase {
-        val dbPath = instrumentation.targetContext.getDatabasePath(name)
-        check(dbPath.exists()) {
-            "Cannot find the database file for $name. " +
-                "Before calling runMigrations, you must first create the database via " +
-                "createDatabase."
+        check(delegate is SupportSQLiteMigrationTestHelper) {
+            "MigrationTestHelper functionality returning a SupportSQLiteDatabase is not possible " +
+                "because a SQLiteDriver was provided during configuration."
         }
-        val schemaBundle = loadSchema(version)
-        val container = RoomDatabase.MigrationContainer()
-        container.addMigrations(*migrations)
-        val autoMigrations = getAutoMigrations(specs)
-        autoMigrations.forEach { autoMigration ->
-            val migrationExists = container.contains(
-                autoMigration.startVersion,
-                autoMigration.endVersion
-            )
-            if (!migrationExists) {
-                container.addMigrations(autoMigration)
-            }
-        }
-        databaseConfiguration = DatabaseConfiguration(
-            context = instrumentation.targetContext,
-            name = name,
-            sqliteOpenHelperFactory = openFactory,
-            migrationContainer = container,
-            callbacks = null,
-            allowMainThreadQueries = true,
-            journalMode = RoomDatabase.JournalMode.TRUNCATE,
-            queryExecutor = ArchTaskExecutor.getIOThreadExecutor(),
-            transactionExecutor = ArchTaskExecutor.getIOThreadExecutor(),
-            multiInstanceInvalidationServiceIntent = null,
-            requireMigration = true,
-            allowDestructiveMigrationOnDowngrade = false,
-            migrationNotRequiredFrom = emptySet(),
-            copyFromAssetPath = null,
-            copyFromFile = null,
-            copyFromInputStream = null,
-            prepackagedDatabaseCallback = null,
-            typeConverters = emptyList(),
-            autoMigrationSpecs = emptyList(),
-            allowDestructiveMigrationForAllTables = false,
-            sqliteDriver = null
-        )
-        @Suppress("DEPRECATION") // Due to RoomOpenHelper
-        val roomOpenHelper = androidx.room.RoomOpenHelper(
-            configuration = databaseConfiguration,
-            delegate = MigratingDelegate(
-                databaseBundle = schemaBundle.database,
-                // we pass the same hash twice since an old schema does not necessarily have
-                // a legacy hash and we would not even persist it.
-                mVerifyDroppedTables = validateDroppedTables
-            ),
-            identityHash = schemaBundle.database.identityHash,
-            legacyHash = schemaBundle.database.identityHash
-        )
-        return openDatabase(name, roomOpenHelper)
-    }
-
-    /**
-     * Returns a list of [Migration] of a database that has been generated using
-     * [androidx.room.AutoMigration].
-     */
-    private fun getAutoMigrations(userProvidedSpecs: List<AutoMigrationSpec>): List<Migration> {
-        if (databaseClass == null) {
-            return if (userProvidedSpecs.isEmpty()) {
-                // TODO: Detect that there are auto migrations to test when a deprecated
-                //  constructor is used.
-                Log.e(
-                    TAG, "If you have any AutoMigrations in your implementation, you must use " +
-                        "a non-deprecated MigrationTestHelper constructor to provide the " +
-                        "Database class in order to test them. If you do not have any " +
-                        "AutoMigrations to test, you may ignore this warning."
-                )
-                mutableListOf()
-            } else {
-                error(
-                    "You must provide the database class in the " +
-                        "MigrationTestHelper constructor in order to test auto migrations."
-                )
-            }
-        }
-        val db: RoomDatabase = findAndInstantiateDatabaseImpl(databaseClass)
-        val requiredAutoMigrationSpecs = db.getRequiredAutoMigrationSpecClasses()
-        return db.createAutoMigrations(
-            createAutoMigrationSpecMap(requiredAutoMigrationSpecs, userProvidedSpecs)
+        return delegate.runMigrationsAndValidate(
+            name, version, validateDroppedTables, migrations
         )
     }
 
     /**
-     * Maps auto migration spec classes to their provided instance.
+     * Creates the database at the given version.
+     *
+     * Once a database is created it can further validate with [runMigrationsAndValidate].
+     *
+     * @param version The version of the schema at which the database should be created.
+     * @return A database connection of the newly created database.
+     * @throws IllegalStateException If a new database was not created.
      */
-    private fun createAutoMigrationSpecMap(
-        requiredAutoMigrationSpecs: Set<KClass<out AutoMigrationSpec>>,
-        userProvidedSpecs: List<AutoMigrationSpec>
-    ): Map<KClass<out AutoMigrationSpec>, AutoMigrationSpec> {
-        if (requiredAutoMigrationSpecs.isEmpty()) {
-            return emptyMap()
+    actual fun createDatabase(version: Int): SQLiteConnection {
+        check(delegate is SQLiteDriverMigrationTestHelper) {
+            "MigrationTestHelper functionality returning a SQLiteConnection is not possible " +
+                "because a SupportSQLiteOpenHelper was provided during configuration (i.e. no " +
+                "SQLiteDriver was provided)."
         }
-        return buildMap {
-            requiredAutoMigrationSpecs.forEach { spec ->
-                val match = userProvidedSpecs.firstOrNull { provided ->
-                    spec.java.isAssignableFrom(provided.javaClass)
-                }
-                require(match != null) {
-                    "A required auto migration spec (${spec.qualifiedName}) has not been provided."
-                }
-                put(spec, match)
-            }
-        }
+        return delegate.createDatabase(version)
     }
 
-    private fun openDatabase(
-        name: String,
-        @Suppress("DEPRECATION")
-        roomOpenHelper: androidx.room.RoomOpenHelper
-    ): SupportSQLiteDatabase {
-        val config = SupportSQLiteOpenHelper.Configuration.builder(instrumentation.targetContext)
-            .callback(roomOpenHelper)
-            .name(name)
-            .build()
-        val db = openFactory.create(config).writableDatabase
-        managedDatabases.add(WeakReference(db))
-        return db
+    /**
+     * Runs the given set of migrations on the existing database once created via [createDatabase].
+     *
+     * This function uses the same algorithm that Room performs to choose migrations such that the
+     * [migrations] instances provided must be sufficient to bring the database from current
+     * version to the desired version. If the database contains
+     * [androidx.room.AutoMigration]s, then those are already included in the list of migrations
+     * to execute if necessary. Note that provided manual migrations take precedence over
+     * auto migrations if they overlap in migration paths.
+     *
+     * Once migrations are done, this functions validates the database schema to ensure the
+     * migration performed resulted in the expected schema.
+     *
+     * @param version The final version the database should migrate to.
+     * @param migrations The list of migrations used to attempt the database migration.
+     * @return A database connection of the migrated database.
+     * @throws IllegalStateException If the schema validation fails.
+     */
+    actual fun runMigrationsAndValidate(
+        version: Int,
+        migrations: List<Migration>,
+    ): SQLiteConnection {
+        check(delegate is SQLiteDriverMigrationTestHelper) {
+            "MigrationTestHelper functionality returning a SQLiteConnection is not possible " +
+                "because a SupportSQLiteOpenHelper was provided during configuration (i.e. no " +
+                "SQLiteDriver was provided)."
+        }
+        return delegate.runMigrationsAndValidate(version, migrations)
     }
 
     override fun finished(description: Description?) {
         super.finished(description)
-        managedDatabases.forEach { dbRef ->
-            val db = dbRef.get()
-            if (db != null && db.isOpen) {
-                try {
-                    db.close()
-                } catch (ignored: Throwable) {
-                }
-            }
-        }
-        managedRoomDatabases.forEach { dbRef ->
-            val roomDatabase = dbRef.get()
-            roomDatabase?.close()
-        }
+        delegate.finished()
+        managedSupportDatabases.forEach { it.get()?.close() }
+        managedRoomDatabases.forEach { it.get()?.close() }
     }
 
     /**
      * Registers a database connection to be automatically closed when the test finishes.
      *
-     * This only works if `MigrationTestHelper` is registered as a Junit test rule via
-     * [Rule][org.junit.Rule] annotation.
+     * This only works if [MigrationTestHelper] is registered as a Junit test rule via
+     * the [org.junit.Rule] annotation.
      *
      * @param db The database connection that should be closed after the test finishes.
      */
@@ -406,14 +343,14 @@
                 " the test starts. Maybe you forgot to annotate MigrationTestHelper as a" +
                 " test rule? (@Rule)"
         }
-        managedDatabases.add(WeakReference(db))
+        managedSupportDatabases.add(WeakReference(db))
     }
 
     /**
      * Registers a database connection to be automatically closed when the test finishes.
      *
-     * This only works if `MigrationTestHelper` is registered as a Junit test rule via
-     * [Rule][org.junit.Rule] annotation.
+     * This only works if [MigrationTestHelper] is registered as a Junit test rule via
+     * the [org.junit.Rule] annotation.
      *
      * @param db The RoomDatabase instance which holds the database.
      */
@@ -425,18 +362,28 @@
         }
         managedRoomDatabases.add(WeakReference(db))
     }
+}
 
-    private fun loadSchema(version: Int): SchemaBundle {
+/**
+ * Base implementation of Android's [MigrationTestHelper]
+ */
+private sealed class AndroidMigrationTestHelper(
+    private val instrumentation: Instrumentation,
+    private val assetsFolder: String
+) {
+    protected val managedConnections = mutableListOf<WeakReference<SQLiteConnection>>()
+
+    fun finished() {
+        managedConnections.forEach { it.get()?.close() }
+    }
+
+    protected fun loadSchema(version: Int): SchemaBundle {
         return try {
             loadSchema(instrumentation.context, version)
-        } catch (testAssetsIOExceptions: FileNotFoundException) {
-            Log.w(
-                TAG, "Could not find the schema file in the test assets. Checking the" +
-                    " application assets"
-            )
+        } catch (testAssetsNotFoundEx: FileNotFoundException) {
             try {
                 loadSchema(instrumentation.targetContext, version)
-            } catch (appAssetsException: FileNotFoundException) {
+            } catch (appAssetsNotFoundEx: FileNotFoundException) {
                 // throw the test assets exception instead
                 throw FileNotFoundException(
                     "Cannot find the schema file in the assets folder. " +
@@ -444,153 +391,221 @@
                         "inputs. See " +
                         "https://developer.android.com/training/data-storage/room/" +
                         "migrating-db-versions#export-schema for details. Missing file: " +
-                        testAssetsIOExceptions.message
+                        testAssetsNotFoundEx.message
                 )
             }
         }
     }
 
-    private fun loadSchema(context: Context, version: Int): SchemaBundle {
+    protected fun loadSchema(context: Context, version: Int): SchemaBundle {
         val input = context.assets.open("$assetsFolder/$version.json")
-        return deserialize(input)
+        return SchemaBundle.deserialize(input)
     }
 
-    @Suppress("DEPRECATION") // Due to RoomOpenHelper
-    internal class MigratingDelegate(
-        databaseBundle: DatabaseBundle,
-        private val mVerifyDroppedTables: Boolean
-    ) : RoomOpenHelperDelegate(databaseBundle) {
-        override fun createAllTables(db: SupportSQLiteDatabase) {
-            throw UnsupportedOperationException(
-                "Was expecting to migrate but received create." +
-                    "Make sure you have created the database first."
-            )
-        }
+    protected fun createDatabaseConfiguration(
+        container: RoomDatabase.MigrationContainer,
+        openFactory: SupportSQLiteOpenHelper.Factory?,
+        sqliteDriver: SQLiteDriver?
+    ) = DatabaseConfiguration(
+        context = instrumentation.targetContext,
+        name = null,
+        sqliteOpenHelperFactory = openFactory,
+        migrationContainer = container,
+        callbacks = null,
+        allowMainThreadQueries = true,
+        journalMode = RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING,
+        queryExecutor = ArchTaskExecutor.getIOThreadExecutor(),
+        transactionExecutor = ArchTaskExecutor.getIOThreadExecutor(),
+        multiInstanceInvalidationServiceIntent = null,
+        requireMigration = true,
+        allowDestructiveMigrationOnDowngrade = false,
+        migrationNotRequiredFrom = emptySet(),
+        copyFromAssetPath = null,
+        copyFromFile = null,
+        copyFromInputStream = null,
+        prepackagedDatabaseCallback = null,
+        typeConverters = emptyList(),
+        autoMigrationSpecs = emptyList(),
+        allowDestructiveMigrationForAllTables = false,
+        sqliteDriver = sqliteDriver,
+        queryCoroutineContext = null
+    )
+}
 
-        override fun onValidateSchema(
-            db: SupportSQLiteDatabase
-        ): androidx.room.RoomOpenHelper.ValidationResult {
-            val tables = mDatabaseBundle.entitiesByTableName
-            tables.values.forEach { entity ->
-                if (entity is FtsEntityBundle) {
-                    val expected = entity.toFtsTableInfo()
-                    val found = FtsTableInfo.read(db, entity.tableName)
-                    if (expected != found) {
-                        return androidx.room.RoomOpenHelper.ValidationResult(
-                            false,
-                            """ ${expected.name.trimEnd()}
-                                |
-                                |Expected:
-                                |
-                                |$expected
-                                |
-                                |Found:
-                                |
-                                |$found
-                            """.trimMargin()
-                        )
-                    }
-                } else {
-                    val expected = entity.toTableInfo()
-                    val found = TableInfo.read(db, entity.tableName)
-                    if (expected != found) {
-                        return androidx.room.RoomOpenHelper.ValidationResult(
-                            false,
-                            """ ${expected.name.trimEnd()}
-                                |
-                                |Expected:
-                                |
-                                |$expected
-                                |
-                                |Found:
-                                |
-                                |$found
-                            """.trimMargin()
-                        )
-                    }
-                }
-            }
-            mDatabaseBundle.views.forEach { view ->
-                val expected = view.toViewInfo()
-                val found = ViewInfo.read(db, view.viewName)
-                if (expected != found) {
-                    return androidx.room.RoomOpenHelper.ValidationResult(
-                        false,
-                        """ ${expected.name.trimEnd()}
-                                |
-                                |Expected: $expected
-                                |
-                                |Found: $found
-                            """.trimMargin()
-                    )
-                }
-            }
-            if (mVerifyDroppedTables) {
-                // now ensure tables that should be removed are removed.
-                val expectedTables = buildSet {
-                    tables.values.forEach { entity ->
-                        add(entity.tableName)
-                        if (entity is FtsEntityBundle) {
-                            addAll(entity.shadowTableNames)
-                        }
-                    }
-                }
-                db.query(
-                    "SELECT name FROM sqlite_master WHERE type='table'" +
-                        " AND name NOT IN(?, ?, ?)",
-                    arrayOf(
-                        Room.MASTER_TABLE_NAME, "android_metadata",
-                        "sqlite_sequence"
-                    )
-                ).useCursor { cursor ->
-                    while (cursor.moveToNext()) {
-                        val tableName = cursor.getString(0)
-                        if (!expectedTables.contains(tableName)) {
-                            return androidx.room.RoomOpenHelper.ValidationResult(
-                                false, "Unexpected table $tableName"
-                            )
-                        }
-                    }
-                }
-            }
-            return androidx.room.RoomOpenHelper.ValidationResult(true, null)
-        }
-    }
+/**
+ * Compatibility implementation of the [MigrationTestHelper] for [SupportSQLiteOpenHelper] and
+ * [SupportSQLiteDatabase].
+ */
+private class SupportSQLiteMigrationTestHelper(
+    instrumentation: Instrumentation,
+    assetsFolder: String,
+    databaseClass: Class<out RoomDatabase>?,
+    private val openFactory: SupportSQLiteOpenHelper.Factory,
+    private val autoMigrationSpecs: List<AutoMigrationSpec>,
+) : AndroidMigrationTestHelper(instrumentation, assetsFolder) {
 
-    @Suppress("DEPRECATION") // Due to RoomOpenHelper
-    internal class CreatingDelegate(
-        databaseBundle: DatabaseBundle
-    ) : RoomOpenHelperDelegate(databaseBundle) {
-        override fun createAllTables(db: SupportSQLiteDatabase) {
-            mDatabaseBundle.buildCreateQueries().forEach { query ->
-                db.execSQL(query)
+    private val context = instrumentation.targetContext
+    private val databaseInstance: RoomDatabase = if (databaseClass == null) {
+        object : RoomDatabase() {
+            override fun createInvalidationTracker(): InvalidationTracker {
+                return InvalidationTracker(this, emptyMap(), emptyMap())
+            }
+            override fun clearAllTables() {
+                error("Function should never be called during tests.")
+            }
+            override fun createAutoMigrations(
+                autoMigrationSpecs: Map<KClass<out AutoMigrationSpec>, AutoMigrationSpec>
+            ): List<Migration> {
+                return emptyList()
             }
         }
-
-        override fun onValidateSchema(
-            db: SupportSQLiteDatabase
-        ): androidx.room.RoomOpenHelper.ValidationResult {
-            throw UnsupportedOperationException(
-                "This open helper just creates the database but it received a migration request."
-            )
-        }
+    } else {
+        findAndInstantiateDatabaseImpl(databaseClass)
     }
 
-    @Suppress("DEPRECATION") // Due to RoomOpenHelper
-    internal abstract class RoomOpenHelperDelegate(
-        val mDatabaseBundle: DatabaseBundle
-    ) : androidx.room.RoomOpenHelper.Delegate(
-            mDatabaseBundle.version
-        ) {
-        override fun dropAllTables(db: SupportSQLiteDatabase) {
-            throw UnsupportedOperationException("cannot drop all tables in the test")
+    fun createDatabase(name: String, version: Int): SupportSQLiteDatabase {
+        val dbPath = context.getDatabasePath(name)
+        if (dbPath.exists()) {
+            check(dbPath.delete()) {
+                "There is a database file and I could not delete it."
+            }
+        }
+        val schemaBundle = loadSchema(version)
+        val connection = createDatabaseCommon(
+            schema = schemaBundle.database,
+            configurationFactory = ::createConfiguration,
+            connectionManagerFactory = { config, openDelegate ->
+                SupportTestConnectionManager(config.copy(name = name), openDelegate)
+            }
+        )
+        managedConnections.add(WeakReference(connection))
+        check(connection is SupportSQLiteConnection) {
+            "Expected connection to be a SupportSQLiteConnection but was ${connection::class}"
+        }
+        return connection.db
+    }
+
+    fun runMigrationsAndValidate(
+        name: String,
+        version: Int,
+        validateDroppedTables: Boolean,
+        migrations: Array<out Migration>
+    ): SupportSQLiteDatabase {
+        val dbPath = context.getDatabasePath(name)
+        check(dbPath.exists()) {
+            "Cannot find the database file for $name. " +
+                "Before calling runMigrations, you must first create the database via " +
+                "createDatabase()."
+        }
+        val schemaBundle = loadSchema(version)
+        val connection = runMigrationsAndValidateCommon(
+            databaseInstance = databaseInstance,
+            schema = schemaBundle.database,
+            migrations = migrations.toList(),
+            autoMigrationSpecs = autoMigrationSpecs,
+            validateUnknownTables = validateDroppedTables,
+            configurationFactory = ::createConfiguration,
+            connectionManagerFactory = { config, openDelegate ->
+                SupportTestConnectionManager(config.copy(name = name), openDelegate)
+            }
+        )
+        managedConnections.add(WeakReference(connection))
+        check(connection is SupportSQLiteConnection) {
+            "Expected connection to be a SupportSQLiteConnection but was ${connection::class}"
+        }
+        return connection.db
+    }
+
+    private fun createConfiguration(container: RoomDatabase.MigrationContainer) =
+        createDatabaseConfiguration(container, openFactory, null)
+
+    private class SupportTestConnectionManager(
+        override val configuration: DatabaseConfiguration,
+        override val openDelegate: RoomOpenDelegate
+    ) : TestConnectionManager() {
+
+        private val driverWrapper: SQLiteDriver
+
+        init {
+            val openFactory = checkNotNull(configuration.sqliteOpenHelperFactory)
+            val openHelperConfig =
+                SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
+                    .name(configuration.name)
+                    .callback(SupportOpenHelperCallback(openDelegate.version))
+                    .build()
+            val supportDriver = SupportSQLiteDriver(openFactory.create(openHelperConfig))
+            this.driverWrapper = DriverWrapper(supportDriver)
         }
 
-        override fun onCreate(db: SupportSQLiteDatabase) {}
-        override fun onOpen(db: SupportSQLiteDatabase) {}
+        override fun openConnection() = driverWrapper.open()
+
+        inner class SupportOpenHelperCallback(
+            version: Int
+        ) : SupportSQLiteOpenHelper.Callback(version) {
+            override fun onCreate(db: SupportSQLiteDatabase) {
+                [email protected](
+                    SupportSQLiteConnection(db)
+                )
+            }
+
+            override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
+                [email protected](
+                    SupportSQLiteConnection(db), oldVersion, newVersion
+                )
+            }
+
+            override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
+                this.onUpgrade(db, oldVersion, newVersion)
+            }
+
+            override fun onOpen(db: SupportSQLiteDatabase) {
+                [email protected](SupportSQLiteConnection(db))
+            }
+        }
+    }
+}
+
+/**
+ * Implementation of the [MigrationTestHelper] for [SQLiteDriver] and [SQLiteConnection].
+ */
+private class SQLiteDriverMigrationTestHelper(
+    instrumentation: Instrumentation,
+    assetsFolder: String,
+    private val driver: SQLiteDriver,
+    databaseClass: KClass<out RoomDatabase>,
+    databaseFactory: () -> RoomDatabase,
+    private val autoMigrationSpecs: List<AutoMigrationSpec>
+) : AndroidMigrationTestHelper(instrumentation, assetsFolder) {
+
+    private val databaseInstance = databaseClass.cast(databaseFactory.invoke())
+
+    fun createDatabase(version: Int): SQLiteConnection {
+        val schemaBundle = loadSchema(version)
+        val connection = createDatabaseCommon(
+            schema = schemaBundle.database,
+            configurationFactory = ::createConfiguration
+        )
+        managedConnections.add(WeakReference(connection))
+        return connection
     }
 
-    internal companion object {
-        private const val TAG = "MigrationTestHelper"
+    fun runMigrationsAndValidate(
+        version: Int,
+        migrations: List<Migration>,
+    ): SQLiteConnection {
+        val schemaBundle = loadSchema(version)
+        val connection = runMigrationsAndValidateCommon(
+            databaseInstance = databaseInstance,
+            schema = schemaBundle.database,
+            migrations = migrations,
+            autoMigrationSpecs = autoMigrationSpecs,
+            validateUnknownTables = false,
+            configurationFactory = ::createConfiguration
+        )
+        managedConnections.add(WeakReference(connection))
+        return connection
     }
+
+    private fun createConfiguration(container: RoomDatabase.MigrationContainer) =
+        createDatabaseConfiguration(container, null, driver)
 }
diff --git a/room/room-testing/src/androidMain/kotlin/androidx/room/testing/BundleUtil.android.kt b/room/room-testing/src/commonMain/kotlin/androidx/room/testing/BundleUtil.kt
similarity index 92%
rename from room/room-testing/src/androidMain/kotlin/androidx/room/testing/BundleUtil.android.kt
rename to room/room-testing/src/commonMain/kotlin/androidx/room/testing/BundleUtil.kt
index 96ef65a..a9a5390 100644
--- a/room/room-testing/src/androidMain/kotlin/androidx/room/testing/BundleUtil.android.kt
+++ b/room/room-testing/src/commonMain/kotlin/androidx/room/testing/BundleUtil.kt
@@ -20,6 +20,7 @@
 package androidx.room.testing
 
 import androidx.annotation.RestrictTo
+import androidx.room.migration.bundle.BaseEntityBundle
 import androidx.room.migration.bundle.DatabaseViewBundle
 import androidx.room.migration.bundle.EntityBundle
 import androidx.room.migration.bundle.FieldBundle
@@ -29,6 +30,7 @@
 import androidx.room.util.FtsTableInfo
 import androidx.room.util.TableInfo
 import androidx.room.util.ViewInfo
+import kotlin.jvm.JvmName
 
 internal fun EntityBundle.toTableInfo(): TableInfo {
     return TableInfo(
@@ -62,8 +64,8 @@
         TableInfo.Index(
             name = bundle.name,
             unique = bundle.isUnique,
-            columns = bundle.columnNames!!,
-            orders = bundle.orders!!
+            columns = bundle.columnNames ?: emptyList(),
+            orders = bundle.orders ?: emptyList()
         )
     }.toSet()
     return result
@@ -85,7 +87,7 @@
     return result
 }
 
-private fun EntityBundle.toColumnNamesSet(): Set<String> {
+private fun BaseEntityBundle.toColumnNamesSet(): Set<String> {
     return this.fields.map { field -> field.columnName }.toSet()
 }
 
diff --git a/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt b/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt
new file mode 100644
index 0000000..4470798
--- /dev/null
+++ b/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt
@@ -0,0 +1,366 @@
+/*
+ * 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.testing
+
+import androidx.room.BaseRoomConnectionManager
+import androidx.room.DatabaseConfiguration
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.RoomOpenDelegate
+import androidx.room.Transactor
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.FtsEntityBundle
+import androidx.room.util.FtsTableInfo
+import androidx.room.util.TableInfo
+import androidx.room.util.ViewInfo
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.execSQL
+import androidx.sqlite.use
+import kotlin.reflect.KClass
+import kotlin.reflect.safeCast
+
+/**
+ * A class that can help test and verify database creation and migration at different versions with
+ * different schemas.
+ *
+ * Common usage of this helper is to create a database at an older version first and then
+ * attempt a migration and validation:
+ * ```
+ * @Test
+ * fun migrationTest() {
+ *   val migrationTestHelper = getMigrationTestHelper()
+ *   // Create the database at version 1
+ *   val newConnection = migrationTestHelper.createDatabase(1)
+ *   // Insert some data that should be preserved
+ *   newConnection.execSQL("INSERT INTO Pet (id, name) VALUES (1, 'Tom')")
+ *   newConnection.close()
+ *
+ *   // Migrate the database to version 2
+ *   val migratedConnection =
+ *       migrationTestHelper.runMigrationsAndValidate(2, listOf(MIGRATION_1_2)))
+ *   migratedConnection.prepare("SELECT * FROM Pet).use { stmt ->
+ *     // Validates data is preserved between migrations.
+ *     assertThat(stmt.step()).isTrue()
+ *     assertThat(stmt.getText(1)).isEqualTo("Tom")
+ *   }
+ *   migratedConnection.close()
+ * }
+ * ```
+ *
+ * The helper relies on exported schemas so [androidx.room.Database.exportSchema] should
+ * be enabled. Schema location should be configured via Room's Gradle Plugin (id 'androidx.room'):
+ * ```
+ * room {
+ *   schemaDirectory("$projectDir/schemas")
+ * }
+ * ```
+ * The helper is then instantiated to use the same schema location where they are exported to. See
+ * platform-specific documentation for further configuration.
+ */
+expect class MigrationTestHelper {
+    /**
+     * Creates the database at the given version.
+     *
+     * Once a database is created it can further validate with [runMigrationsAndValidate].
+     *
+     * @param version The version of the schema at which the database should be created.
+     * @return A database connection of the newly created database.
+     * @throws IllegalStateException If a new database was not created.
+     */
+    fun createDatabase(version: Int): SQLiteConnection
+
+    /**
+     * Runs the given set of migrations on the existing database once created via [createDatabase].
+     *
+     * This function uses the same algorithm that Room performs to choose migrations such that the
+     * [migrations] instances provided must be sufficient to bring the database from current
+     * version to the desired version. If the database contains
+     * [androidx.room.AutoMigration]s, then those are already included in the list of migrations
+     * to execute if necessary. Note that provided manual migrations take precedence over
+     * auto migrations if they overlap in migration paths.
+     *
+     * Once migrations are done, this functions validates the database schema to ensure the
+     * migration performed resulted in the expected schema.
+     *
+     * @param version The final version the database should migrate to.
+     * @param migrations The list of migrations used to attempt the database migration.
+     * @return A database connection of the migrated database.
+     * @throws IllegalStateException If the schema validation fails.
+     */
+    fun runMigrationsAndValidate(
+        version: Int,
+        migrations: List<Migration> = emptyList()
+    ): SQLiteConnection
+}
+
+internal typealias ConnectionManagerFactory =
+        (DatabaseConfiguration, RoomOpenDelegate) -> TestConnectionManager
+
+internal typealias ConfigurationFactory =
+        (RoomDatabase.MigrationContainer) -> DatabaseConfiguration
+
+/**
+ * Common logic for [MigrationTestHelper.createDatabase]
+ */
+internal fun createDatabaseCommon(
+    schema: DatabaseBundle,
+    configurationFactory: ConfigurationFactory,
+    connectionManagerFactory: ConnectionManagerFactory = { config, openDelegate ->
+        DefaultTestConnectionManager(config, openDelegate)
+    }
+): SQLiteConnection {
+    val emptyContainer = RoomDatabase.MigrationContainer()
+    val configuration = configurationFactory.invoke(emptyContainer)
+    val testConnectionManager = connectionManagerFactory.invoke(
+        configuration, CreateOpenDelegate(schema)
+    )
+    return testConnectionManager.openConnection()
+}
+
+/**
+ * Common logic for [MigrationTestHelper.runMigrationsAndValidate]
+ */
+internal fun runMigrationsAndValidateCommon(
+    databaseInstance: RoomDatabase,
+    schema: DatabaseBundle,
+    migrations: List<Migration>,
+    autoMigrationSpecs: List<AutoMigrationSpec>,
+    validateUnknownTables: Boolean,
+    configurationFactory: ConfigurationFactory,
+    connectionManagerFactory: ConnectionManagerFactory = { config, openDelegate ->
+        DefaultTestConnectionManager(config, openDelegate)
+    }
+): SQLiteConnection {
+    val container = RoomDatabase.MigrationContainer()
+    container.addMigrations(migrations)
+    val autoMigrations = getAutoMigrations(databaseInstance, autoMigrationSpecs)
+    autoMigrations.forEach { autoMigration ->
+        val migrationExists = container.contains(
+            autoMigration.startVersion,
+            autoMigration.endVersion
+        )
+        if (!migrationExists) {
+            container.addMigration(autoMigration)
+        }
+    }
+    val configuration = configurationFactory.invoke(container)
+    val testConnectionManager = connectionManagerFactory.invoke(
+        configuration, MigrateOpenDelegate(schema, validateUnknownTables)
+    )
+    return testConnectionManager.openConnection()
+}
+
+private fun getAutoMigrations(
+    databaseInstance: RoomDatabase,
+    providedSpecs: List<AutoMigrationSpec>
+): List<Migration> {
+    val autoMigrationSpecMap =
+        createAutoMigrationSpecMap(
+            databaseInstance.getRequiredAutoMigrationSpecClasses(),
+            providedSpecs
+        )
+    return databaseInstance.createAutoMigrations(autoMigrationSpecMap)
+}
+
+private fun createAutoMigrationSpecMap(
+    requiredAutoMigrationSpecs: Set<KClass<out AutoMigrationSpec>>,
+    providedSpecs: List<AutoMigrationSpec>
+): Map<KClass<out AutoMigrationSpec>, AutoMigrationSpec> {
+    if (requiredAutoMigrationSpecs.isEmpty()) {
+        return emptyMap()
+    }
+    return buildMap {
+        requiredAutoMigrationSpecs.forEach { spec ->
+            val match = providedSpecs.firstOrNull { provided ->
+                spec.safeCast(provided) != null
+            }
+            requireNotNull(match) {
+                "A required auto migration spec (${spec.qualifiedName}) has not been provided."
+            }
+            put(spec, match)
+        }
+    }
+}
+
+internal abstract class TestConnectionManager : BaseRoomConnectionManager() {
+    override val callbacks: List<RoomDatabase.Callback> = emptyList()
+
+    override suspend fun <R> useConnection(
+        isReadOnly: Boolean,
+        block: suspend (Transactor) -> R
+    ): R {
+        error("Function should never be invoked during tests.")
+    }
+
+    abstract fun openConnection(): SQLiteConnection
+}
+
+private class DefaultTestConnectionManager(
+    override val configuration: DatabaseConfiguration,
+    override val openDelegate: RoomOpenDelegate
+) : TestConnectionManager() {
+
+    private val driverWrapper = DriverWrapper(requireNotNull(configuration.sqliteDriver))
+
+    override fun openConnection() = driverWrapper.open()
+}
+
+private sealed class TestOpenDelegate(
+    databaseBundle: DatabaseBundle
+) : RoomOpenDelegate(databaseBundle.version, databaseBundle.identityHash) {
+    override fun onCreate(connection: SQLiteConnection) {}
+    override fun onPreMigrate(connection: SQLiteConnection) {}
+    override fun onPostMigrate(connection: SQLiteConnection) {}
+    override fun onOpen(connection: SQLiteConnection) {}
+
+    override fun dropAllTables(connection: SQLiteConnection) {
+        error("Can't drop all tables during a test.")
+    }
+}
+
+private class CreateOpenDelegate(
+    val databaseBundle: DatabaseBundle
+) : TestOpenDelegate(databaseBundle) {
+    private var createAllTables = false
+
+    override fun onOpen(connection: SQLiteConnection) {
+        check(createAllTables) {
+            "Creation of tables didn't occur while creating a new database. A database at the " +
+                "driver configured path likely already exists. Did you forget to delete it?"
+        }
+    }
+
+    override fun onValidateSchema(connection: SQLiteConnection): ValidationResult {
+        error("Validation of schemas should never occur while creating a new database.")
+    }
+
+    override fun createAllTables(connection: SQLiteConnection) {
+        databaseBundle.buildCreateQueries().forEach { createSql ->
+            connection.execSQL(createSql)
+        }
+        createAllTables = true
+    }
+}
+
+private class MigrateOpenDelegate(
+    val databaseBundle: DatabaseBundle,
+    val validateUnknownTables: Boolean
+) : TestOpenDelegate(databaseBundle) {
+    override fun onValidateSchema(connection: SQLiteConnection): ValidationResult {
+        val tables = databaseBundle.entitiesByTableName
+        tables.values.forEach { entity ->
+            when (entity) {
+                is EntityBundle -> {
+                    val expected = entity.toTableInfo()
+                    val found = TableInfo.read(connection, entity.tableName)
+                    if (expected != found) {
+                        return ValidationResult(
+                            isValid = false,
+                            expectedFoundMsg =
+                                """ ${expected.name.trimEnd()}
+                                |
+                                |Expected:
+                                |
+                                |$expected
+                                |
+                                |Found:
+                                |
+                                |$found
+                                """.trimMargin()
+                        )
+                    }
+                }
+                is FtsEntityBundle -> {
+                    val expected = entity.toFtsTableInfo()
+                    val found = FtsTableInfo.read(connection, entity.tableName)
+                    if (expected != found) {
+                        return ValidationResult(
+                            isValid = false,
+                            expectedFoundMsg =
+                                """ ${expected.name.trimEnd()}
+                                |
+                                |Expected:
+                                |
+                                |$expected
+                                |
+                                |Found:
+                                |
+                                |$found
+                                """.trimMargin()
+                        )
+                    }
+                }
+            }
+        }
+        databaseBundle.views.forEach { view ->
+            val expected = view.toViewInfo()
+            val found = ViewInfo.read(connection, view.viewName)
+            if (expected != found) {
+                return ValidationResult(
+                    isValid = false,
+                    expectedFoundMsg =
+                        """ ${expected.name.trimEnd()}
+                        |
+                        |Expected: $expected
+                        |
+                        |Found: $found
+                        """.trimMargin()
+                )
+            }
+        }
+        if (validateUnknownTables) {
+            val expectedTables = buildSet {
+                tables.values.forEach { entity ->
+                    add(entity.tableName)
+                    if (entity is FtsEntityBundle) {
+                        addAll(entity.shadowTableNames)
+                    }
+                }
+            }
+            connection.prepare(
+                """
+                SELECT name FROM sqlite_master
+                WHERE type = 'table' AND name NOT IN (?, ?, ?)
+                """.trimIndent()
+            ).use { statement ->
+                statement.bindText(1, Room.MASTER_TABLE_NAME)
+                statement.bindText(2, "sqlite_sequence")
+                statement.bindText(3, "android_metadata")
+                while (statement.step()) {
+                    val tableName = statement.getText(0)
+                    if (!expectedTables.contains(tableName)) {
+                        return ValidationResult(
+                            isValid = false,
+                            expectedFoundMsg = "Unexpected table $tableName"
+                        )
+                    }
+                }
+            }
+        }
+        return ValidationResult(true, null)
+    }
+
+    override fun createAllTables(connection: SQLiteConnection) {
+        error(
+            "Creation of tables should never occur while validating migrations. Did you forget " +
+                "to first create the database?"
+        )
+    }
+}
diff --git a/room/room-testing/src/jvmMain/kotlin/androidx/room/testing/MigrationTestHelper.jvm.kt b/room/room-testing/src/jvmMain/kotlin/androidx/room/testing/MigrationTestHelper.jvm.kt
new file mode 100644
index 0000000..dc6f6e1
--- /dev/null
+++ b/room/room-testing/src/jvmMain/kotlin/androidx/room/testing/MigrationTestHelper.jvm.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.testing
+
+import androidx.room.DatabaseConfiguration
+import androidx.room.RoomDatabase
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.migration.bundle.SchemaBundle
+import androidx.room.util.findAndInstantiateDatabaseImpl
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteDriver
+import java.nio.file.Path
+import kotlin.io.path.inputStream
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * A class that can help test and verify database creation and migration at different versions with
+ * different schemas.
+ *
+ * Common usage of this helper is to create a database at an older version first and then
+ * attempt a migration and validation:
+ * ```
+ * @get:Rule
+ * val migrationTestHelper = MigrationTestHelper(
+ *    schemaDirectoryPath = Path("schemas")
+ *    driver = sqliteDriver,
+ *    databaseClass = PetDatabase::class
+ * )
+ *
+ * @Test
+ * fun migrationTest() {
+ *   // Create the database at version 1
+ *   val newConnection = migrationTestHelper.createDatabase(1)
+ *   // Insert some data that should be preserved
+ *   newConnection.execSQL("INSERT INTO Pet (id, name) VALUES (1, 'Tom')")
+ *   newConnection.close()
+ *
+ *   // Migrate the database to version 2
+ *   val migratedConnection =
+ *       migrationTestHelper.runMigrationsAndValidate(2, listOf(MIGRATION_1_2)))
+ *   migratedConnection.prepare("SELECT * FROM Pet).use { stmt ->
+ *     // Validates data is preserved between migrations.
+ *     assertThat(stmt.step()).isTrue()
+ *     assertThat(stmt.getText(1)).isEqualTo("Tom")
+ *   }
+ *   migratedConnection.close()
+ * }
+ * ```
+ *
+ * The helper relies on exported schemas so [androidx.room.Database.exportSchema] should
+ * be enabled. Schema location should be configured via Room's Gradle Plugin (id 'androidx.room'):
+ * ```
+ * room {
+ *   schemaDirectory("$projectDir/schemas")
+ * }
+ * ```
+ * The [schemaDirectoryPath] must match the exported schema location for this helper to properly
+ * create and validate schemas.
+ *
+ * @param schemaDirectoryPath The schema directory where schema files are exported.
+ * @param driver A driver that opens connection to a file database. A driver that opens connections
+ * to an in-memory database would be meaningless.
+ * @param databaseClass The [androidx.room.Database] annotated class.
+ * @param databaseFactory An optional factory function to create an instance of the [databaseClass].
+ * Should be the same factory used when building the database via [androidx.room.Room.databaseBuilder].
+ * @param autoMigrationSpecs The list of [androidx.room.ProvidedAutoMigrationSpec] instances
+ * for [androidx.room.AutoMigration]s that require them.
+ */
+actual class MigrationTestHelper(
+    private val schemaDirectoryPath: Path,
+    private val driver: SQLiteDriver,
+    private val databaseClass: KClass<out RoomDatabase>,
+    databaseFactory: () -> RoomDatabase = {
+        findAndInstantiateDatabaseImpl(databaseClass.java)
+    },
+    private val autoMigrationSpecs: List<AutoMigrationSpec> = emptyList()
+) : TestWatcher() {
+
+    private val databaseInstance = databaseClass.cast(databaseFactory.invoke())
+    private val managedConnections = mutableListOf<SQLiteConnection>()
+
+    /**
+     * Creates the database at the given version.
+     *
+     * Once a database is created it can further validate with [runMigrationsAndValidate].
+     *
+     * @param version The version of the schema at which the database should be created.
+     * @return A database connection of the newly created database.
+     * @throws IllegalStateException If a new database was not created.
+     */
+    actual fun createDatabase(version: Int): SQLiteConnection {
+        val schemaBundle = loadSchema(version)
+        val connection = createDatabaseCommon(
+            schema = schemaBundle.database,
+            configurationFactory = ::createDatabaseConfiguration
+        )
+        managedConnections.add(connection)
+        return connection
+    }
+
+    /**
+     * Runs the given set of migrations on the existing database once created via [createDatabase].
+     *
+     * This function uses the same algorithm that Room performs to choose migrations such that the
+     * [migrations] instances provided must be sufficient to bring the database from current
+     * version to the desired version. If the database contains
+     * [androidx.room.AutoMigration]s, then those are already included in the list of migrations
+     * to execute if necessary. Note that provided manual migrations take precedence over
+     * auto migrations if they overlap in migration paths.
+     *
+     * Once migrations are done, this functions validates the database schema to ensure the
+     * migration performed resulted in the expected schema.
+     *
+     * @param version The final version the database should migrate to.
+     * @param migrations The list of migrations used to attempt the database migration.
+     * @throws IllegalStateException If the schema validation fails.
+     */
+    actual fun runMigrationsAndValidate(
+        version: Int,
+        migrations: List<Migration>,
+    ): SQLiteConnection {
+        val schemaBundle = loadSchema(version)
+        val connection = runMigrationsAndValidateCommon(
+            databaseInstance = databaseInstance,
+            schema = schemaBundle.database,
+            migrations = migrations,
+            autoMigrationSpecs = autoMigrationSpecs,
+            validateUnknownTables = false,
+            configurationFactory = ::createDatabaseConfiguration
+        )
+        managedConnections.add(connection)
+        return connection
+    }
+
+    override fun finished(description: Description?) {
+        super.finished(description)
+        managedConnections.forEach(SQLiteConnection::close)
+    }
+
+    private fun loadSchema(version: Int): SchemaBundle {
+        val databaseFQN = checkNotNull(databaseClass.qualifiedName)
+        val schemaPath = schemaDirectoryPath.resolve(databaseFQN).resolve("$version.json")
+        return schemaPath.inputStream().use { SchemaBundle.deserialize(it) }
+    }
+
+    private fun createDatabaseConfiguration(
+        container: RoomDatabase.MigrationContainer,
+    ) = DatabaseConfiguration(
+        name = null,
+        migrationContainer = container,
+        callbacks = null,
+        journalMode = RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING,
+        requireMigration = true,
+        allowDestructiveMigrationOnDowngrade = false,
+        migrationNotRequiredFrom = null,
+        typeConverters = emptyList(),
+        autoMigrationSpecs = emptyList(),
+        sqliteDriver = driver,
+        queryCoroutineContext = null
+    )
+}
diff --git a/room/room-testing/src/nativeMain/kotlin/androidx/room/testing/MigrationTestHelper.native.kt b/room/room-testing/src/nativeMain/kotlin/androidx/room/testing/MigrationTestHelper.native.kt
new file mode 100644
index 0000000..90747171
--- /dev/null
+++ b/room/room-testing/src/nativeMain/kotlin/androidx/room/testing/MigrationTestHelper.native.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.testing
+
+import androidx.room.DatabaseConfiguration
+import androidx.room.RoomDatabase
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.migration.bundle.SchemaBundle
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteDriver
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+import okio.FileSystem
+import okio.Path.Companion.toPath
+
+/**
+ * A class that can help test and verify database creation and migration at different versions with
+ * different schemas.
+ *
+ * Common usage of this helper is to create a database at an older version first and then
+ * attempt a migration and validation:
+ * ```
+ * private val migrationTestHelper = MigrationTestHelper(
+ *    schemaDirectoryPath = Path("schemas")
+ *    driver = sqliteDriver,
+ *    databaseClass = PetDatabase::class,
+ *    databaseFactory = { PetDatabase::class.instantiateImpl() }
+ * )
+ *
+ * @AfterTest
+ * fun after() {
+ *   migrationTestHelper.finished()
+ * }
+ *
+ * @Test
+ * fun migrationTest() {
+ *   // Create the database at version 1
+ *   val newConnection = migrationTestHelper.createDatabase(1)
+ *   // Insert some data that should be preserved
+ *   newConnection.execSQL("INSERT INTO Pet (id, name) VALUES (1, 'Tom')")
+ *   newConnection.close()
+ *
+ *   // Migrate the database to version 2
+ *   val migratedConnection =
+ *       migrationTestHelper.runMigrationsAndValidate(2, listOf(MIGRATION_1_2)))
+ *   migratedConnection.prepare("SELECT * FROM Pet).use { stmt ->
+ *     // Validates data is preserved between migrations.
+ *     assertThat(stmt.step()).isTrue()
+ *     assertThat(stmt.getText(1)).isEqualTo("Tom")
+ *   }
+ *   migratedConnection.close()
+ * }
+ * ```
+ *
+ * The helper relies on exported schemas so [androidx.room.Database.exportSchema] should
+ * be enabled. Schema location should be configured via Room's Gradle Plugin (id 'androidx.room'):
+ * ```
+ * room {
+ *   schemaDirectory("$projectDir/schemas")
+ * }
+ * ```
+ * The [schemaDirectoryPath] must match the exported schema location for this helper to properly
+ * create and validate schemas.
+ *
+ * @param schemaDirectoryPath The schema directory where schema files are exported.
+ * @param driver A driver that opens connection to a file database. A driver that opens connections
+ * to an in-memory database would be meaningless.
+ * @param databaseClass The [androidx.room.Database] annotated class.
+ * @param databaseFactory The factory function to create an instance of the [databaseClass]. Should
+ * be the same factory used when building the database via [androidx.room.Room.databaseBuilder].
+ * @param autoMigrationSpecs The list of [androidx.room.ProvidedAutoMigrationSpec] instances
+ * for [androidx.room.AutoMigration]s that require them.
+ */
+actual class MigrationTestHelper(
+    private val schemaDirectoryPath: String,
+    private val driver: SQLiteDriver,
+    private val databaseClass: KClass<out RoomDatabase>,
+    databaseFactory: () -> RoomDatabase,
+    private val autoMigrationSpecs: List<AutoMigrationSpec> = emptyList()
+) {
+    private val databaseInstance = databaseClass.cast(databaseFactory.invoke())
+    private val managedConnections = mutableListOf<SQLiteConnection>()
+
+    /**
+     * Creates the database at the given version.
+     *
+     * Once a database is created it can further validate with [runMigrationsAndValidate].
+     *
+     * @param version The version of the schema at which the database should be created.
+     * @return A database connection of the newly created database.
+     * @throws IllegalStateException If a new database was not created.
+     */
+    actual fun createDatabase(version: Int): SQLiteConnection {
+        val schemaBundle = loadSchema(version)
+        val connection = createDatabaseCommon(
+            schema = schemaBundle.database,
+            configurationFactory = ::createDatabaseConfiguration
+        )
+        managedConnections.add(connection)
+        return connection
+    }
+
+    /**
+     * Runs the given set of migrations on the existing database once created via [createDatabase].
+     *
+     * This function uses the same algorithm that Room performs to choose migrations such that the
+     * [migrations] instances provided must be sufficient to bring the database from current
+     * version to the desired version. If the database contains
+     * [androidx.room.AutoMigration]s, then those are already included in the list of migrations
+     * to execute if necessary. Note that provided manual migrations take precedence over
+     * auto migrations if they overlap in migration paths.
+     *
+     * Once migrations are done, this functions validates the database schema to ensure the
+     * migration performed resulted in the expected schema.
+     *
+     * @param version The final version the database should migrate to.
+     * @param migrations The list of migrations used to attempt the database migration.
+     * @throws IllegalStateException If the schema validation fails.
+     */
+    actual fun runMigrationsAndValidate(
+        version: Int,
+        migrations: List<Migration>,
+    ): SQLiteConnection {
+        val schemaBundle = loadSchema(version)
+        val connection = runMigrationsAndValidateCommon(
+            databaseInstance = databaseInstance,
+            schema = schemaBundle.database,
+            migrations = migrations,
+            autoMigrationSpecs = autoMigrationSpecs,
+            validateUnknownTables = false,
+            configurationFactory = ::createDatabaseConfiguration
+        )
+        managedConnections.add(connection)
+        return connection
+    }
+
+    fun finished() {
+        managedConnections.forEach(SQLiteConnection::close)
+    }
+
+    private fun loadSchema(version: Int): SchemaBundle {
+        val databaseFQN = checkNotNull(databaseClass.qualifiedName)
+        val schemaPath = schemaDirectoryPath.toPath().resolve(databaseFQN).resolve("$version.json")
+        return FileSystem.SYSTEM.read(schemaPath) { SchemaBundle.deserialize(this) }
+    }
+
+    private fun createDatabaseConfiguration(
+        container: RoomDatabase.MigrationContainer,
+    ) = DatabaseConfiguration(
+        name = null,
+        migrationContainer = container,
+        callbacks = null,
+        journalMode = RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING,
+        requireMigration = true,
+        allowDestructiveMigrationOnDowngrade = false,
+        migrationNotRequiredFrom = null,
+        typeConverters = emptyList(),
+        autoMigrationSpecs = emptyList(),
+        sqliteDriver = driver,
+        queryCoroutineContext = null
+    )
+}
diff --git a/sharetarget/sharetarget/api/restricted_current.txt b/sharetarget/sharetarget/api/restricted_current.txt
index 950bca2..a481389 100644
--- a/sharetarget/sharetarget/api/restricted_current.txt
+++ b/sharetarget/sharetarget/api/restricted_current.txt
@@ -6,7 +6,7 @@
     method public java.util.List<android.service.chooser.ChooserTarget!>! onGetChooserTargets(android.content.ComponentName!, android.content.IntentFilter!);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ShortcutInfoCompatSaverImpl extends androidx.core.content.pm.ShortcutInfoCompatSaver<com.google.common.util.concurrent.ListenableFuture<java.lang.Void>> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ShortcutInfoCompatSaverImpl extends androidx.core.content.pm.ShortcutInfoCompatSaver<com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>!> {
     method @AnyThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>! addShortcuts(java.util.List<androidx.core.content.pm.ShortcutInfoCompat!>!);
     method @AnyThread public static androidx.sharetarget.ShortcutInfoCompatSaverImpl! getInstance(android.content.Context!);
     method @WorkerThread public androidx.core.graphics.drawable.IconCompat! getShortcutIcon(String!) throws java.lang.Exception;
diff --git a/slice/slice-remotecallback/api/current.txt b/slice/slice-remotecallback/api/current.txt
index 5eed69e..01a91d4 100644
--- a/slice/slice-remotecallback/api/current.txt
+++ b/slice/slice-remotecallback/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.slice.remotecallback {
 
-  public abstract class RemoteSliceProvider<T extends androidx.slice.remotecallback.RemoteSliceProvider> extends androidx.slice.SliceProvider implements androidx.remotecallback.CallbackReceiver<T> {
+  public abstract class RemoteSliceProvider<T extends androidx.slice.remotecallback.RemoteSliceProvider> extends androidx.slice.SliceProvider implements androidx.remotecallback.CallbackReceiver<T!> {
     ctor public RemoteSliceProvider();
     method public T createRemoteCallback(android.content.Context);
   }
diff --git a/slice/slice-remotecallback/api/restricted_current.txt b/slice/slice-remotecallback/api/restricted_current.txt
index aec2a8d..90ba6d5 100644
--- a/slice/slice-remotecallback/api/restricted_current.txt
+++ b/slice/slice-remotecallback/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.slice.remotecallback {
 
-  public abstract class RemoteSliceProvider<T extends androidx.slice.remotecallback.RemoteSliceProvider> extends androidx.slice.SliceProvider implements androidx.remotecallback.CallbackBase<T> androidx.remotecallback.CallbackReceiver<T> {
+  public abstract class RemoteSliceProvider<T extends androidx.slice.remotecallback.RemoteSliceProvider> extends androidx.slice.SliceProvider implements androidx.remotecallback.CallbackBase<T!> androidx.remotecallback.CallbackReceiver<T!> {
     ctor public RemoteSliceProvider();
     method public T createRemoteCallback(android.content.Context);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.remotecallback.RemoteCallback toRemoteCallback(Class<T!>, android.content.Context, String, android.os.Bundle, String);
diff --git a/slice/slice-view/api/current.txt b/slice/slice-view/api/current.txt
index c1cd45f..e3e7cad 100644
--- a/slice/slice-view/api/current.txt
+++ b/slice/slice-view/api/current.txt
@@ -147,7 +147,7 @@
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
   }
 
-  @Deprecated public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
+  @Deprecated public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder!> {
     ctor @Deprecated public SliceAdapter(android.content.Context);
     method @Deprecated public androidx.slice.widget.GridRowView getGridRowView();
     method @Deprecated public int getItemCount();
@@ -176,7 +176,7 @@
     method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
   }
 
-  @Deprecated public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
+  @Deprecated public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice!> {
     method @Deprecated public void goLive();
     method @Deprecated public void parseStream();
   }
@@ -192,7 +192,7 @@
   @Deprecated @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
   }
 
-  @Deprecated public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
+  @Deprecated public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice!> android.view.View.OnClickListener {
     ctor @Deprecated public SliceView(android.content.Context!);
     ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?);
     ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?, int);
diff --git a/slice/slice-view/api/restricted_current.txt b/slice/slice-view/api/restricted_current.txt
index 0864f0a..3a64b6f 100644
--- a/slice/slice-view/api/restricted_current.txt
+++ b/slice/slice-view/api/restricted_current.txt
@@ -205,7 +205,7 @@
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
   }
 
-  @Deprecated public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
+  @Deprecated public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder!> {
     ctor @Deprecated public SliceAdapter(android.content.Context);
     method @Deprecated public androidx.slice.widget.GridRowView getGridRowView();
     method @Deprecated public int getItemCount();
@@ -235,7 +235,7 @@
     method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
   }
 
-  @Deprecated public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
+  @Deprecated public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice!> {
     method @Deprecated public void goLive();
     method @Deprecated public void parseStream();
   }
@@ -251,7 +251,7 @@
   @Deprecated @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
   }
 
-  @Deprecated public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
+  @Deprecated public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice!> android.view.View.OnClickListener {
     ctor @Deprecated public SliceView(android.content.Context!);
     ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?);
     ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?, int);
diff --git a/sqlite/integration-tests/driver-conformance-test/build.gradle b/sqlite/integration-tests/driver-conformance-test/build.gradle
index 7dfaa55..58a482a 100644
--- a/sqlite/integration-tests/driver-conformance-test/build.gradle
+++ b/sqlite/integration-tests/driver-conformance-test/build.gradle
@@ -31,7 +31,6 @@
 
 androidXMultiplatform {
     android()
-    androidNative()
     ios()
     jvm()
     linux()
diff --git a/sqlite/sqlite-bundled/build.gradle b/sqlite/sqlite-bundled/build.gradle
index c7f512f..8df8903 100644
--- a/sqlite/sqlite-bundled/build.gradle
+++ b/sqlite/sqlite-bundled/build.gradle
@@ -143,7 +143,7 @@
         prepareSqliteSourcesTask.map { it.destinationDirectory }
     )
 
-    // Defince C++ compilation of JNI
+    // Define C++ compilation of JNI
     def jvmArtJniImplementation = createNativeCompilation("jvmArtJniImplementation") {
         configureEachTarget { nativeCompilation ->
             // add JNI headers as sources
@@ -169,7 +169,6 @@
     android() {
         addNativeLibrariesToJniLibs(it, jvmArtJniImplementation)
     }
-    androidNative()
     ios()
     jvm() {
         addNativeLibrariesToResources(it, jvmArtJniImplementation)
diff --git a/sqlite/sqlite-framework/build.gradle b/sqlite/sqlite-framework/build.gradle
index 9f4c995..0add91b 100644
--- a/sqlite/sqlite-framework/build.gradle
+++ b/sqlite/sqlite-framework/build.gradle
@@ -21,10 +21,11 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
+
 import androidx.build.PlatformIdentifier
 import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import org.jetbrains.kotlin.konan.target.KonanTarget
+import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
     id("AndroidXPlugin")
@@ -53,7 +54,6 @@
 
 androidXMultiplatform {
     android()
-    androidNative()
     ios() {
         // Link to sqlite3 available in iOS
         binaries.all {
@@ -119,14 +119,11 @@
                 test.defaultSourceSet {
                     dependsOn(nativeTest)
                 }
-                if (target.konanTarget == KonanTarget.LINUX_X64.INSTANCE) {
+                if (target.konanTarget.family == Family.LINUX) {
                     // For tests in Linux host, statically include androidx's compiled SQLite
                     // via a generated C interop definition
                     createCinteropFromArchiveConfiguration(test, configurations["sqliteSharedArchive"])
-                } else if (
-                    target.konanTarget == KonanTarget.MACOS_X64.INSTANCE ||
-                        target.konanTarget == KonanTarget.MACOS_ARM64.INSTANCE
-                ) {
+                } else if (target.konanTarget.family == Family.OSX) {
                     // For tests in Mac host, link to shared SQLite library included in MacOS
                     test.kotlinOptions.freeCompilerArgs += [
                         "-linker-options", "-lsqlite3"
diff --git a/sqlite/sqlite/build.gradle b/sqlite/sqlite/build.gradle
index 9a1b291..ce04df8 100644
--- a/sqlite/sqlite/build.gradle
+++ b/sqlite/sqlite/build.gradle
@@ -34,7 +34,6 @@
 
 androidXMultiplatform {
     android()
-    androidNative()
     ios()
     jvm()
     linux()
diff --git a/stableaidl/stableaidl-gradle-plugin/build.gradle b/stableaidl/stableaidl-gradle-plugin/build.gradle
index efce547..13fded7 100644
--- a/stableaidl/stableaidl-gradle-plugin/build.gradle
+++ b/stableaidl/stableaidl-gradle-plugin/build.gradle
@@ -31,11 +31,18 @@
 
 apply from: "../../buildSrc/kotlin-dsl-dependency.gradle"
 
+configurations {
+    // Config for plugin classpath to be used during tests
+    testPlugin {
+        canBeConsumed = false
+        canBeResolved = true
+    }
+}
+
 dependencies {
     implementation(findGradleKotlinDsl())
     implementation(gradleApi())
     implementation(libs.androidGradlePluginApi)
-    implementation(libs.androidGradlePluginz) // Needed for BaseExtension, see b/268237729.
     implementation(libs.androidToolsCommon)
     implementation(libs.androidToolsRepository)
     implementation(libs.androidToolsSdkCommon)
@@ -44,6 +51,9 @@
     implementation(libs.guava)
     implementation(libs.kotlinStdlib)
 
+    testPlugin(libs.androidGradlePluginz)
+
+    testImplementation(libs.androidGradlePluginz)
     testImplementation(gradleTestKit())
     testImplementation(project(":internal-testutils-gradle-plugin"))
     testImplementation(libs.androidToolsAnalyticsProtos)
@@ -53,6 +63,10 @@
     testImplementation(libs.truth)
 }
 
+tasks.withType(PluginUnderTestMetadata.class).named("pluginUnderTestMetadata").configure {
+    it.pluginClasspath.from(configurations.testPlugin)
+}
+
 gradlePlugin {
     plugins {
         stableAidl {
diff --git a/stableaidl/stableaidl-gradle-plugin/lint-baseline.xml b/stableaidl/stableaidl-gradle-plugin/lint-baseline.xml
index 6ce5472..24cc85a 100644
--- a/stableaidl/stableaidl-gradle-plugin/lint-baseline.xml
+++ b/stableaidl/stableaidl-gradle-plugin/lint-baseline.xml
@@ -1,14 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha09" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha09)" variant="all" version="8.4.0-alpha09">
-
-    <issue
-        id="InternalGradleApiUsage"
-        message="Avoid using internal Android Gradle Plugin APIs"
-        errorLine1="import com.android.build.gradle.internal.LoggerWrapper"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/stableaidl/internal/process/GradleProcessExecutor.kt"/>
-    </issue>
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
 
     <issue
         id="InternalGradleApiUsage"
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlExtensionImpl.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlExtensionImpl.kt
index fa45f3c..ed441f7 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlExtensionImpl.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlExtensionImpl.kt
@@ -21,7 +21,6 @@
 import androidx.stableaidl.tasks.StableAidlCheckApi
 import androidx.stableaidl.tasks.UpdateStableAidlApiTask
 import com.android.build.api.variant.SourceDirectories
-import com.android.build.gradle.internal.tasks.factory.dependsOn
 import java.io.File
 import org.gradle.api.Task
 import org.gradle.api.tasks.TaskProvider
@@ -67,4 +66,14 @@
 
     internal val importSourceDirs = mutableListOf<SourceDirectories.Flat>()
     internal val allTasks = mutableMapOf<String, Set<TaskProvider<*>>>()
+
+    internal fun <T : Task> TaskProvider<out T>.dependsOn(
+        vararg tasks: TaskProvider<out Task>
+    ): TaskProvider<out T> {
+        if (tasks.isEmpty().not()) {
+            configure { it.dependsOn(*tasks) }
+        }
+
+        return this
+    }
 }
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
index 754d5e4..6ea7e33 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
@@ -21,7 +21,6 @@
 import com.android.build.api.variant.AndroidComponentsExtension
 import com.android.build.api.variant.DslExtension
 import com.android.build.api.variant.Variant
-import com.android.build.gradle.BaseExtension
 import com.android.utils.usLocaleCapitalize
 import org.gradle.api.GradleException
 import org.gradle.api.Plugin
@@ -196,23 +195,9 @@
  */
 internal const val SOURCE_TYPE_STABLE_AIDL_IMPORTS = "stableAidlImports"
 
-internal fun SdkComponents.aidl(baseExtension: BaseExtension): Provider<RegularFile> =
-    sdkDirectory.map {
-        it.dir("build-tools").dir(baseExtension.buildToolsVersion).file(
-            if (java.lang.System.getProperty("os.name").startsWith("Windows")) {
-                "aidl.exe"
-            } else {
-                "aidl"
-            }
-        )
-    }
-
-internal fun SdkComponents.aidlFramework(baseExtension: BaseExtension): Provider<RegularFile> =
-    sdkDirectory.map {
-        it.dir("platforms")
-            .dir(baseExtension.compileSdkVersion!!)
-            .file("framework.aidl")
-    }
+internal fun SdkComponents.aidl(): Provider<RegularFile> =
+    @Suppress("UnstableApiUsage")
+    aidl.flatMap { it.executable }
 
 /**
  * Returns the AIDL import directories for the given variant of the project.
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/internal/process/GradleProcessExecutor.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/internal/process/GradleProcessExecutor.kt
index 76e13bd..d789156 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/internal/process/GradleProcessExecutor.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/internal/process/GradleProcessExecutor.kt
@@ -15,7 +15,7 @@
  */
 package androidx.stableaidl.internal.process
 
-import com.android.build.gradle.internal.LoggerWrapper
+import androidx.stableaidl.internal.LoggerWrapper
 import com.android.ide.common.process.ProcessException
 import com.android.ide.common.process.ProcessExecutor
 import com.android.ide.common.process.ProcessInfo
diff --git a/test/uiautomator/uiautomator/api/current.txt b/test/uiautomator/uiautomator/api/current.txt
index e6772e0..89cf12f6 100644
--- a/test/uiautomator/uiautomator/api/current.txt
+++ b/test/uiautomator/uiautomator/api/current.txt
@@ -132,7 +132,7 @@
     method public void sendStatus(int, android.os.Bundle);
   }
 
-  public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
+  public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable!,U!> {
     ctor public SearchCondition();
   }
 
@@ -356,7 +356,7 @@
     method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
   }
 
-  public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
+  public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2!,U!> {
     ctor public UiObject2Condition();
   }
 
diff --git a/test/uiautomator/uiautomator/api/restricted_current.txt b/test/uiautomator/uiautomator/api/restricted_current.txt
index e6772e0..89cf12f6 100644
--- a/test/uiautomator/uiautomator/api/restricted_current.txt
+++ b/test/uiautomator/uiautomator/api/restricted_current.txt
@@ -132,7 +132,7 @@
     method public void sendStatus(int, android.os.Bundle);
   }
 
-  public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
+  public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable!,U!> {
     ctor public SearchCondition();
   }
 
@@ -356,7 +356,7 @@
     method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
   }
 
-  public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
+  public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2!,U!> {
     ctor public UiObject2Condition();
   }
 
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ImmersiveList.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ImmersiveList.kt
index 94240b6..da94fd9 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ImmersiveList.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ImmersiveList.kt
@@ -24,7 +24,12 @@
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusRestorer
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.semantics.CollectionItemInfo
 import androidx.compose.ui.semantics.collectionItemInfo
@@ -43,7 +48,7 @@
     }
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
 @Composable
 private fun SampleImmersiveList() {
     val immersiveListHeight = 300.dp
@@ -68,13 +73,13 @@
             )
         }
     ) {
-        val focusRestorerModifiers = createCustomInitialFocusRestorerModifiers()
+        val focusRequester = remember { FocusRequester() }
 
         LazyRow(
             horizontalArrangement = Arrangement.spacedBy(cardSpacing),
             modifier = Modifier
                 .lazyListSemantics(1, backgrounds.count())
-                .then(focusRestorerModifiers.parentModifier)
+                .focusRestorer { focusRequester }
         ) {
             itemsIndexed(backgrounds) { index, backgroundColor ->
                 Card(
@@ -83,7 +88,7 @@
                             collectionItemInfo = CollectionItemInfo(0, 1, index, 1)
                         }
                         .immersiveListItem(index)
-                        .ifElse(index == 0, focusRestorerModifiers.childModifier),
+                        .ifElse(index == 0, Modifier.focusRequester(focusRequester)),
                     backgroundColor = backgroundColor
                 )
             }
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
index 20134a5..ed832f8 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
@@ -22,7 +22,11 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusRestorer
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.semantics.CollectionInfo
@@ -56,22 +60,23 @@
     }
 }
 
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun SampleLazyRow(modifier: Modifier = Modifier) {
     val colors = listOf(Color.Red, Color.Magenta, Color.Green, Color.Yellow, Color.Blue, Color.Cyan)
     val backgroundColors = List(columnsCount) { colors.random() }
-    val focusRestorerModifiers = createCustomInitialFocusRestorerModifiers()
+    val focusRequester = remember { FocusRequester() }
 
     TvLazyRow(
         modifier = modifier
             .lazyListSemantics(1, columnsCount)
-            .then(focusRestorerModifiers.parentModifier),
+            .focusRestorer { focusRequester },
         horizontalArrangement = Arrangement.spacedBy(10.dp)
     ) {
         itemsIndexed(backgroundColors) { index, item ->
             Card(
                 modifier = Modifier
-                    .ifElse(index == 0, focusRestorerModifiers.childModifier)
+                    .ifElse(index == 0, Modifier.focusRequester(focusRequester))
                     .semantics {
                         collectionItemInfo = CollectionItemInfo(0, 1, index, 1)
                     },
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TopNavigation.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TopNavigation.kt
index f0ed752..f065bb2 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TopNavigation.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TopNavigation.kt
@@ -26,7 +26,11 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusRestorer
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.tv.material3.ExperimentalTvMaterial3Api
@@ -86,27 +90,25 @@
 /**
  * Pill indicator tab row for reference
  */
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
 @Composable
 fun PillIndicatorTabRow(
     tabs: List<String>,
     selectedTabIndex: Int,
     updateSelectedTab: (Int) -> Unit
 ) {
-    val focusRestorerModifiers = createCustomInitialFocusRestorerModifiers()
+    val focusRequester = remember { FocusRequester() }
 
     TabRow(
         selectedTabIndex = selectedTabIndex,
-        modifier = Modifier
-            .then(focusRestorerModifiers.parentModifier)
+        modifier = Modifier.focusRestorer { focusRequester }
     ) {
         tabs.forEachIndexed { index, tab ->
             key(index) {
                 Tab(
                     selected = index == selectedTabIndex,
                     onFocus = { updateSelectedTab(index) },
-                    modifier = Modifier
-                        .ifElse(index == 0, focusRestorerModifiers.childModifier)
+                    modifier = Modifier.ifElse(index == 0, Modifier.focusRequester(focusRequester))
                 ) {
                     Text(
                         text = tab,
@@ -122,14 +124,14 @@
 /**
  * Underlined indicator tab row for reference
  */
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
 @Composable
 fun UnderlinedIndicatorTabRow(
     tabs: List<String>,
     selectedTabIndex: Int,
     updateSelectedTab: (Int) -> Unit
 ) {
-    val focusRestorerModifiers = createCustomInitialFocusRestorerModifiers()
+    val focusRequester = remember { FocusRequester() }
 
     TabRow(
         selectedTabIndex = selectedTabIndex,
@@ -141,14 +143,14 @@
             )
         },
         modifier = Modifier
-            .then(focusRestorerModifiers.parentModifier),
+            .focusRestorer { focusRequester },
     ) {
         tabs.forEachIndexed { index, tab ->
             Tab(
                 selected = index == selectedTabIndex,
                 onFocus = { updateSelectedTab(index) },
                 modifier = Modifier
-                    .ifElse(index == 0, focusRestorerModifiers.childModifier),
+                    .ifElse(index == 0, Modifier.focusRequester(focusRequester)),
                 colors = TabDefaults.underlinedIndicatorTabColors(),
             ) {
                 Text(
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/createCustomInitialFocusRestorerModifiers.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/createCustomInitialFocusRestorerModifiers.kt
deleted file mode 100644
index 3f3acbf..0000000
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/createCustomInitialFocusRestorerModifiers.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.playground
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusProperties
-import androidx.compose.ui.focus.focusRequester
-
-/**
- * Assign the parentModifier to the container of items and assign the childModifier to the
- * item that needs to first gain focus. For example, if you want the item at index 0 to get
- * focus for the first time, you can do the following:
- *
- * LazyRow(modifier.then(modifiers.parentModifier) {
- *   item1(modifier.then(modifiers.childModifier) {...}
- *   item2 {...}
- *   item3 {...}
- *   ...
- * }
- */
-data class FocusRequesterModifiers(
-    val parentModifier: Modifier,
-    val childModifier: Modifier
-)
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Composable
-fun createCustomInitialFocusRestorerModifiers(): FocusRequesterModifiers {
-    val focusRequester = remember { FocusRequester() }
-    val childFocusRequester = remember { FocusRequester() }
-
-    val parentModifier = Modifier
-        .focusRequester(focusRequester)
-        .focusProperties {
-            exit = {
-                focusRequester.saveFocusedChild()
-                FocusRequester.Default
-            }
-            enter = {
-                if (!focusRequester.restoreFocusedChild())
-                    childFocusRequester
-                else
-                    FocusRequester.Cancel
-            }
-        }
-
-    val childModifier = Modifier.focusRequester(childFocusRequester)
-
-    return FocusRequesterModifiers(parentModifier, childModifier)
-}
diff --git a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
index d09d324..b697996 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
@@ -19,10 +19,8 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -57,7 +55,6 @@
 
   TabRow(
     selectedTabIndex = selectedTabIndex,
-    separator = { Spacer(modifier = Modifier.width(12.dp)) },
     modifier = Modifier.focusRestorer()
   ) {
     tabs.forEachIndexed { index, tab ->
@@ -89,7 +86,6 @@
 
   TabRow(
     selectedTabIndex = selectedTabIndex,
-    separator = { Spacer(modifier = Modifier.width(12.dp)) },
     indicator = { tabPositions, doesTabRowHaveFocus ->
       TabRowDefaults.UnderlinedIndicator(
         currentTabPosition = tabPositions[selectedTabIndex],
@@ -108,7 +104,7 @@
           Text(
             text = tab,
             fontSize = 12.sp,
-            modifier = Modifier.padding(bottom = 4.dp)
+            modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
           )
         }
       }
@@ -137,7 +133,6 @@
 
   TabRow(
     selectedTabIndex = selectedTabIndex,
-    separator = { Spacer(modifier = Modifier.width(12.dp)) },
     modifier = Modifier.focusRestorer()
   ) {
     tabs.forEachIndexed { index, tab ->
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index ae5d620..a20cb7d 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -33,13 +33,13 @@
     property public final androidx.tv.material3.Border None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonBorder {
+  @androidx.compose.runtime.Immutable public final class ButtonBorder {
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonColors {
+  @androidx.compose.runtime.Immutable public final class ButtonColors {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonDefaults {
+  public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public androidx.compose.foundation.layout.PaddingValues getButtonWithIconContentPadding();
@@ -56,15 +56,15 @@
     field public static final androidx.tv.material3.ButtonDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonGlow {
+  @androidx.compose.runtime.Immutable public final class ButtonGlow {
   }
 
   public final class ButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder 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.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder 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 @androidx.compose.runtime.NonRestartableComposable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder 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 @androidx.compose.runtime.NonRestartableComposable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder 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.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonScale {
+  @androidx.compose.runtime.Immutable public final class ButtonScale {
     field public static final androidx.tv.material3.ButtonScale.Companion Companion;
   }
 
@@ -73,7 +73,7 @@
     property public final androidx.tv.material3.ButtonScale None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonShape {
+  @androidx.compose.runtime.Immutable public final class ButtonShape {
   }
 
   @androidx.compose.runtime.Immutable public final class CardBorder {
@@ -384,7 +384,7 @@
     property public final androidx.tv.material3.Glow None;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class IconButtonDefaults {
+  public final class IconButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getLargeButtonSize();
@@ -406,8 +406,8 @@
   }
 
   public final class IconButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
   }
 
   public final class IconKt {
@@ -609,7 +609,7 @@
     method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.tv.material3.ColorScheme colorScheme, optional androidx.tv.material3.Shapes shapes, optional androidx.tv.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemBorder {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemBorder {
     ctor public NavigationDrawerItemBorder(androidx.tv.material3.Border border, androidx.tv.material3.Border focusedBorder, androidx.tv.material3.Border pressedBorder, androidx.tv.material3.Border selectedBorder, androidx.tv.material3.Border disabledBorder, androidx.tv.material3.Border focusedSelectedBorder, androidx.tv.material3.Border focusedDisabledBorder, androidx.tv.material3.Border pressedSelectedBorder);
     method public androidx.tv.material3.Border getBorder();
     method public androidx.tv.material3.Border getDisabledBorder();
@@ -629,7 +629,7 @@
     property public final androidx.tv.material3.Border selectedBorder;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemColors {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemColors {
     ctor public NavigationDrawerItemColors(long containerColor, long contentColor, long inactiveContentColor, long focusedContainerColor, long focusedContentColor, long pressedContainerColor, long pressedContentColor, long selectedContainerColor, long selectedContentColor, long disabledContainerColor, long disabledContentColor, long disabledInactiveContentColor, long focusedSelectedContainerColor, long focusedSelectedContentColor, long pressedSelectedContainerColor, long pressedSelectedContentColor);
     method public long getContainerColor();
     method public long getContentColor();
@@ -665,7 +665,7 @@
     property public final long selectedContentColor;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemDefaults {
+  public final class NavigationDrawerItemDefaults {
     method @androidx.compose.runtime.Composable public void TrailingBadge(String text, optional long containerColor, optional long contentColor);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NavigationDrawerItemBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border selectedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedSelectedBorder, optional androidx.tv.material3.Border focusedDisabledBorder, optional androidx.tv.material3.Border pressedSelectedBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NavigationDrawerItemColors colors(optional long containerColor, optional long contentColor, optional long inactiveContentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long selectedContainerColor, optional long selectedContentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledInactiveContentColor, optional long focusedSelectedContainerColor, optional long focusedSelectedContentColor, optional long pressedSelectedContainerColor, optional long pressedSelectedContentColor);
@@ -699,7 +699,7 @@
     field public static final androidx.tv.material3.NavigationDrawerItemDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemGlow {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemGlow {
     ctor public NavigationDrawerItemGlow(androidx.tv.material3.Glow glow, androidx.tv.material3.Glow focusedGlow, androidx.tv.material3.Glow pressedGlow, androidx.tv.material3.Glow selectedGlow, androidx.tv.material3.Glow focusedSelectedGlow, androidx.tv.material3.Glow pressedSelectedGlow);
     method public androidx.tv.material3.Glow getFocusedGlow();
     method public androidx.tv.material3.Glow getFocusedSelectedGlow();
@@ -716,10 +716,10 @@
   }
 
   public final class NavigationDrawerItemKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemScale {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemScale {
     ctor public NavigationDrawerItemScale(@FloatRange(from=0.0) float scale, @FloatRange(from=0.0) float focusedScale, @FloatRange(from=0.0) float pressedScale, @FloatRange(from=0.0) float selectedScale, @FloatRange(from=0.0) float disabledScale, @FloatRange(from=0.0) float focusedSelectedScale, @FloatRange(from=0.0) float focusedDisabledScale, @FloatRange(from=0.0) float pressedSelectedScale);
     method public float getDisabledScale();
     method public float getFocusedDisabledScale();
@@ -745,7 +745,7 @@
     property public final androidx.tv.material3.NavigationDrawerItemScale None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemShape {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemShape {
     ctor public NavigationDrawerItemShape(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape focusedShape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape selectedShape, androidx.compose.ui.graphics.Shape disabledShape, androidx.compose.ui.graphics.Shape focusedSelectedShape, androidx.compose.ui.graphics.Shape focusedDisabledShape, androidx.compose.ui.graphics.Shape pressedSelectedShape);
     method public androidx.compose.ui.graphics.Shape getDisabledShape();
     method public androidx.compose.ui.graphics.Shape getFocusedDisabledShape();
@@ -786,7 +786,7 @@
     field public static final androidx.tv.material3.NonInteractiveSurfaceDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedButtonDefaults {
+  public final class OutlinedButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public androidx.compose.foundation.layout.PaddingValues getButtonWithIconContentPadding();
@@ -803,7 +803,7 @@
     field public static final androidx.tv.material3.OutlinedButtonDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedIconButtonDefaults {
+  public final class OutlinedIconButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getLargeButtonSize();
@@ -971,20 +971,20 @@
     method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit>? onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? thumbContent, optional boolean enabled, optional androidx.tv.material3.SwitchColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabColors {
+  public final class TabColors {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabDefaults {
+  public final class TabDefaults {
     method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long contentColor, optional long inactiveContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledContentColor, optional long disabledInactiveContentColor, optional long disabledSelectedContentColor);
     method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long contentColor, optional long inactiveContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledContentColor, optional long disabledInactiveContentColor, optional long disabledSelectedContentColor);
     field public static final androidx.tv.material3.TabDefaults INSTANCE;
   }
 
   public final class TabKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Tab(androidx.tv.material3.TabRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, 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 Tab(androidx.tv.material3.TabRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabRowDefaults {
+  public final class TabRowDefaults {
     method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean doesTabRowHaveFocus, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
     method @androidx.compose.runtime.Composable public void TabSeparator();
     method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean doesTabRowHaveFocus, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
@@ -995,18 +995,18 @@
   }
 
   public final class TabRowKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function2<? super java.util.List<androidx.compose.ui.unit.DpRect>,? super java.lang.Boolean,kotlin.Unit> indicator, kotlin.jvm.functions.Function1<? super androidx.tv.material3.TabRowScope,kotlin.Unit> tabs);
+    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function2<? super java.util.List<androidx.compose.ui.unit.DpRect>,? super java.lang.Boolean,kotlin.Unit> indicator, kotlin.jvm.functions.Function1<? super androidx.tv.material3.TabRowScope,kotlin.Unit> tabs);
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface TabRowScope {
+  public interface TabRowScope {
     method public boolean getHasFocus();
     property public abstract boolean hasFocus;
   }
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
@@ -1076,10 +1076,10 @@
     property public final androidx.compose.ui.text.TextStyle titleSmall;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class WideButtonContentColor {
+  @androidx.compose.runtime.Immutable public final class WideButtonContentColor {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class WideButtonDefaults {
+  public final class WideButtonDefaults {
     method @androidx.compose.runtime.Composable public void Background(boolean enabled, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.WideButtonContentColor contentColor(optional long color, optional long focusedColor, optional long pressedColor, optional long disabledColor);
@@ -1090,8 +1090,8 @@
   }
 
   public final class WideButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
   }
 
 }
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index ae5d620..a20cb7d 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -33,13 +33,13 @@
     property public final androidx.tv.material3.Border None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonBorder {
+  @androidx.compose.runtime.Immutable public final class ButtonBorder {
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonColors {
+  @androidx.compose.runtime.Immutable public final class ButtonColors {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonDefaults {
+  public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public androidx.compose.foundation.layout.PaddingValues getButtonWithIconContentPadding();
@@ -56,15 +56,15 @@
     field public static final androidx.tv.material3.ButtonDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonGlow {
+  @androidx.compose.runtime.Immutable public final class ButtonGlow {
   }
 
   public final class ButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder 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.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder 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 @androidx.compose.runtime.NonRestartableComposable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder 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 @androidx.compose.runtime.NonRestartableComposable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder 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.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonScale {
+  @androidx.compose.runtime.Immutable public final class ButtonScale {
     field public static final androidx.tv.material3.ButtonScale.Companion Companion;
   }
 
@@ -73,7 +73,7 @@
     property public final androidx.tv.material3.ButtonScale None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonShape {
+  @androidx.compose.runtime.Immutable public final class ButtonShape {
   }
 
   @androidx.compose.runtime.Immutable public final class CardBorder {
@@ -384,7 +384,7 @@
     property public final androidx.tv.material3.Glow None;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class IconButtonDefaults {
+  public final class IconButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getLargeButtonSize();
@@ -406,8 +406,8 @@
   }
 
   public final class IconButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
   }
 
   public final class IconKt {
@@ -609,7 +609,7 @@
     method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.tv.material3.ColorScheme colorScheme, optional androidx.tv.material3.Shapes shapes, optional androidx.tv.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemBorder {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemBorder {
     ctor public NavigationDrawerItemBorder(androidx.tv.material3.Border border, androidx.tv.material3.Border focusedBorder, androidx.tv.material3.Border pressedBorder, androidx.tv.material3.Border selectedBorder, androidx.tv.material3.Border disabledBorder, androidx.tv.material3.Border focusedSelectedBorder, androidx.tv.material3.Border focusedDisabledBorder, androidx.tv.material3.Border pressedSelectedBorder);
     method public androidx.tv.material3.Border getBorder();
     method public androidx.tv.material3.Border getDisabledBorder();
@@ -629,7 +629,7 @@
     property public final androidx.tv.material3.Border selectedBorder;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemColors {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemColors {
     ctor public NavigationDrawerItemColors(long containerColor, long contentColor, long inactiveContentColor, long focusedContainerColor, long focusedContentColor, long pressedContainerColor, long pressedContentColor, long selectedContainerColor, long selectedContentColor, long disabledContainerColor, long disabledContentColor, long disabledInactiveContentColor, long focusedSelectedContainerColor, long focusedSelectedContentColor, long pressedSelectedContainerColor, long pressedSelectedContentColor);
     method public long getContainerColor();
     method public long getContentColor();
@@ -665,7 +665,7 @@
     property public final long selectedContentColor;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemDefaults {
+  public final class NavigationDrawerItemDefaults {
     method @androidx.compose.runtime.Composable public void TrailingBadge(String text, optional long containerColor, optional long contentColor);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NavigationDrawerItemBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border selectedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedSelectedBorder, optional androidx.tv.material3.Border focusedDisabledBorder, optional androidx.tv.material3.Border pressedSelectedBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NavigationDrawerItemColors colors(optional long containerColor, optional long contentColor, optional long inactiveContentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long selectedContainerColor, optional long selectedContentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledInactiveContentColor, optional long focusedSelectedContainerColor, optional long focusedSelectedContentColor, optional long pressedSelectedContainerColor, optional long pressedSelectedContentColor);
@@ -699,7 +699,7 @@
     field public static final androidx.tv.material3.NavigationDrawerItemDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemGlow {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemGlow {
     ctor public NavigationDrawerItemGlow(androidx.tv.material3.Glow glow, androidx.tv.material3.Glow focusedGlow, androidx.tv.material3.Glow pressedGlow, androidx.tv.material3.Glow selectedGlow, androidx.tv.material3.Glow focusedSelectedGlow, androidx.tv.material3.Glow pressedSelectedGlow);
     method public androidx.tv.material3.Glow getFocusedGlow();
     method public androidx.tv.material3.Glow getFocusedSelectedGlow();
@@ -716,10 +716,10 @@
   }
 
   public final class NavigationDrawerItemKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemScale {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemScale {
     ctor public NavigationDrawerItemScale(@FloatRange(from=0.0) float scale, @FloatRange(from=0.0) float focusedScale, @FloatRange(from=0.0) float pressedScale, @FloatRange(from=0.0) float selectedScale, @FloatRange(from=0.0) float disabledScale, @FloatRange(from=0.0) float focusedSelectedScale, @FloatRange(from=0.0) float focusedDisabledScale, @FloatRange(from=0.0) float pressedSelectedScale);
     method public float getDisabledScale();
     method public float getFocusedDisabledScale();
@@ -745,7 +745,7 @@
     property public final androidx.tv.material3.NavigationDrawerItemScale None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemShape {
+  @androidx.compose.runtime.Immutable public final class NavigationDrawerItemShape {
     ctor public NavigationDrawerItemShape(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape focusedShape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape selectedShape, androidx.compose.ui.graphics.Shape disabledShape, androidx.compose.ui.graphics.Shape focusedSelectedShape, androidx.compose.ui.graphics.Shape focusedDisabledShape, androidx.compose.ui.graphics.Shape pressedSelectedShape);
     method public androidx.compose.ui.graphics.Shape getDisabledShape();
     method public androidx.compose.ui.graphics.Shape getFocusedDisabledShape();
@@ -786,7 +786,7 @@
     field public static final androidx.tv.material3.NonInteractiveSurfaceDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedButtonDefaults {
+  public final class OutlinedButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public androidx.compose.foundation.layout.PaddingValues getButtonWithIconContentPadding();
@@ -803,7 +803,7 @@
     field public static final androidx.tv.material3.OutlinedButtonDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedIconButtonDefaults {
+  public final class OutlinedIconButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getLargeButtonSize();
@@ -971,20 +971,20 @@
     method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit>? onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? thumbContent, optional boolean enabled, optional androidx.tv.material3.SwitchColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabColors {
+  public final class TabColors {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabDefaults {
+  public final class TabDefaults {
     method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long contentColor, optional long inactiveContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledContentColor, optional long disabledInactiveContentColor, optional long disabledSelectedContentColor);
     method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long contentColor, optional long inactiveContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledContentColor, optional long disabledInactiveContentColor, optional long disabledSelectedContentColor);
     field public static final androidx.tv.material3.TabDefaults INSTANCE;
   }
 
   public final class TabKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Tab(androidx.tv.material3.TabRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, 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 Tab(androidx.tv.material3.TabRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabRowDefaults {
+  public final class TabRowDefaults {
     method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean doesTabRowHaveFocus, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
     method @androidx.compose.runtime.Composable public void TabSeparator();
     method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, boolean doesTabRowHaveFocus, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
@@ -995,18 +995,18 @@
   }
 
   public final class TabRowKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function2<? super java.util.List<androidx.compose.ui.unit.DpRect>,? super java.lang.Boolean,kotlin.Unit> indicator, kotlin.jvm.functions.Function1<? super androidx.tv.material3.TabRowScope,kotlin.Unit> tabs);
+    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function2<? super java.util.List<androidx.compose.ui.unit.DpRect>,? super java.lang.Boolean,kotlin.Unit> indicator, kotlin.jvm.functions.Function1<? super androidx.tv.material3.TabRowScope,kotlin.Unit> tabs);
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface TabRowScope {
+  public interface TabRowScope {
     method public boolean getHasFocus();
     property public abstract boolean hasFocus;
   }
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
@@ -1076,10 +1076,10 @@
     property public final androidx.compose.ui.text.TextStyle titleSmall;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class WideButtonContentColor {
+  @androidx.compose.runtime.Immutable public final class WideButtonContentColor {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class WideButtonDefaults {
+  public final class WideButtonDefaults {
     method @androidx.compose.runtime.Composable public void Background(boolean enabled, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.WideButtonContentColor contentColor(optional long color, optional long focusedColor, optional long pressedColor, optional long disabledColor);
@@ -1090,8 +1090,8 @@
   }
 
   public final class WideButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
   }
 
 }
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt
index 68299e8..88440a9 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt
@@ -148,8 +148,8 @@
 
     @Test
     fun painter_noIntrinsicSize_dimensions() {
-        val width = 24.dp
-        val height = 24.dp
+        val width = 20.dp
+        val height = 20.dp
         val painter = ColorPainter(Color.Red)
         val testTag = "testTag"
 
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
index 089607a..1c1044f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
@@ -72,7 +72,6 @@
  * still happen internally.
  * @param content the content of the button
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun Button(
@@ -146,7 +145,6 @@
  * still happen internally.
  * @param content the content of the button
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun OutlinedButton(
@@ -181,7 +179,6 @@
     )
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun ButtonImpl(
     onClick: () -> Unit,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonDefaults.kt
index e147851..7c0ee76 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonDefaults.kt
@@ -32,7 +32,6 @@
     val MinHeight = 40.dp
 }
 
-@ExperimentalTvMaterial3Api
 object ButtonDefaults {
     private val ContainerShape = CircleShape
     private val ButtonHorizontalPadding = 16.dp
@@ -54,7 +53,7 @@
     )
 
     /** The default size of the icon when used inside any button. */
-    val IconSize = 18.dp
+    val IconSize = 20.dp
 
     /**
      * The default size of the spacing between an icon and a text when they used inside any button.
@@ -196,7 +195,6 @@
     )
 }
 
-@ExperimentalTvMaterial3Api
 object OutlinedButtonDefaults {
     private val ContainerShape = CircleShape
     private val ButtonHorizontalPadding = 16.dp
@@ -211,7 +209,7 @@
     )
 
     /** The default size of the icon when used inside any button. */
-    val IconSize = 18.dp
+    val IconSize = 20.dp
 
     /**
      * The default size of the spacing between an icon and a text when they used inside any button.
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
index 554dea1..129a288 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
@@ -25,7 +25,6 @@
 /**
  * Defines [Shape] for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonShape internal constructor(
     internal val shape: Shape,
@@ -68,7 +67,6 @@
 /**
  * Defines [Color]s for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonColors internal constructor(
     internal val containerColor: Color,
@@ -124,7 +122,6 @@
 /**
  * Defines [Color]s for all TV [Interaction] states of a WideButton
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class WideButtonContentColor internal constructor(
     internal val contentColor: Color,
@@ -165,7 +162,6 @@
 /**
  * Defines the scale for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonScale internal constructor(
     @FloatRange(from = 0.0) internal val scale: Float,
@@ -221,7 +217,6 @@
 /**
  * Defines [Border] for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonBorder internal constructor(
     internal val border: Border,
@@ -265,7 +260,6 @@
 /**
  * Defines [Glow] for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonGlow internal constructor(
     internal val glow: Glow,
@@ -300,7 +294,6 @@
 
 private val WideButtonContainerColor = Color.Transparent
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonShape.toClickableSurfaceShape(): ClickableSurfaceShape = ClickableSurfaceShape(
     shape = shape,
     focusedShape = focusedShape,
@@ -309,7 +302,6 @@
     focusedDisabledShape = focusedDisabledShape
 )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonColors.toClickableSurfaceColors(): ClickableSurfaceColors =
     ClickableSurfaceColors(
         containerColor = containerColor,
@@ -322,7 +314,6 @@
         disabledContentColor = disabledContentColor
     )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun WideButtonContentColor.toClickableSurfaceColors(): ClickableSurfaceColors =
     ClickableSurfaceColors(
         containerColor = WideButtonContainerColor,
@@ -335,7 +326,6 @@
         disabledContentColor = disabledContentColor
     )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonScale.toClickableSurfaceScale() = ClickableSurfaceScale(
     scale = scale,
     focusedScale = focusedScale,
@@ -344,7 +334,6 @@
     focusedDisabledScale = focusedDisabledScale
 )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonBorder.toClickableSurfaceBorder() = ClickableSurfaceBorder(
     border = border,
     focusedBorder = focusedBorder,
@@ -353,7 +342,6 @@
     focusedDisabledBorder = focusedDisabledBorder
 )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonGlow.toClickableSurfaceGlow() = ClickableSurfaceGlow(
     glow = glow,
     focusedGlow = focusedGlow,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt
index 8d8ce6c..4f4f2a5 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt
@@ -36,7 +36,6 @@
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
 
 /**
  * A Material Design icon component that draws [imageVector] using [tint], with a default value
@@ -169,5 +168,4 @@
 private fun Size.isInfinite() = width.isInfinite() && height.isInfinite()
 
 // Default icon size, for icons with no intrinsic size information
-// TODO(rvighnesh): change this to IconButtonTokens.IconSize when we introduce IconButton
-private val DefaultIconSizeModifier = Modifier.size(24.dp)
+private val DefaultIconSizeModifier = Modifier.size(IconButtonDefaults.MediumIconSize)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
index 790d79b..8108044 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
@@ -60,7 +60,6 @@
  * still happen internally.
  * @param content the content of the button, typically an [Icon]
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun IconButton(
@@ -130,7 +129,6 @@
  * still happen internally.
  * @param content the content of the button, typically an [Icon]
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun OutlinedIconButton(
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt
index d2a3f17..92efdc2 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.unit.dp
 
-@ExperimentalTvMaterial3Api
 object IconButtonDefaults {
     private val ContainerShape = CircleShape
 
@@ -184,7 +183,6 @@
     )
 }
 
-@ExperimentalTvMaterial3Api
 object OutlinedIconButtonDefaults {
     private val ContainerShape = CircleShape
 
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
index e2f574c..7bca093 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
@@ -62,7 +62,6 @@
  * interactions will still happen internally.
  * @param content main content of this composable
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 @Composable
 fun NavigationDrawerScope.NavigationDrawerItem(
     selected: Boolean,
@@ -162,7 +161,6 @@
     )
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun NavigationDrawerItemShape.toToggleableListItemShape() =
     ListItemDefaults.shape(
@@ -176,7 +174,6 @@
         pressedSelectedShape = pressedSelectedShape,
     )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun NavigationDrawerItemColors.toToggleableListItemColors(
     doesNavigationDrawerHaveFocus: Boolean
@@ -199,7 +196,6 @@
         pressedSelectedContentColor = pressedSelectedContentColor,
     )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun NavigationDrawerItemScale.toToggleableListItemScale() =
     ListItemDefaults.scale(
@@ -213,7 +209,6 @@
         pressedSelectedScale = pressedSelectedScale,
     )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun NavigationDrawerItemBorder.toToggleableListItemBorder() =
     ListItemDefaults.border(
@@ -227,7 +222,6 @@
         pressedSelectedBorder = pressedSelectedBorder,
     )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun NavigationDrawerItemGlow.toToggleableListItemGlow() =
     ListItemDefaults.glow(
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
index abe79a8..a5a25e3 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
@@ -40,7 +40,6 @@
 /**
  * Contains the default values used by selectable [NavigationDrawerItem]
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 object NavigationDrawerItemDefaults {
     /**
      * The default Icon size used by [NavigationDrawerItem]
@@ -121,6 +120,7 @@
      * Creates a trailing badge for [NavigationDrawerItem]
      */
     @Composable
+    @OptIn(ExperimentalTvMaterial3Api::class) // TODO: This will be removed once Text API is marked as stable
     fun TrailingBadge(
         text: String,
         containerColor: Color = TrailingBadgeContainerColor,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
index f08e865..5e2dc6e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
@@ -40,7 +40,6 @@
  * @param pressedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 @Immutable
 class NavigationDrawerItemShape(
     val shape: Shape,
@@ -132,7 +131,6 @@
  * @param pressedSelectedContentColor the content color used when the [NavigationDrawerItem] is
  * enabled, pressed and selected
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 @Immutable
 class NavigationDrawerItemColors(
     val containerColor: Color,
@@ -232,7 +230,6 @@
  * @param pressedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 @Immutable
 class NavigationDrawerItemScale(
     @FloatRange(from = 0.0) val scale: Float,
@@ -320,7 +317,6 @@
  * @param pressedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 @Immutable
 class NavigationDrawerItemBorder(
     val border: Border,
@@ -388,7 +384,6 @@
  * @param pressedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 @Immutable
 class NavigationDrawerItemGlow(
     val glow: Glow,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
index 4ab89da35..f62a9ee 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
@@ -16,7 +16,6 @@
 
 package androidx.tv.material3
 
-import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.background
 import androidx.compose.foundation.focusable
@@ -343,13 +342,9 @@
     )
 
     val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
-    val contentColorAsAnim by animateColorAsState(
-        targetValue = contentColor,
-        label = "Surface.contentColor"
-    )
 
     CompositionLocalProvider(
-        LocalContentColor provides contentColorAsAnim,
+        LocalContentColor provides contentColor,
         LocalAbsoluteTonalElevation provides absoluteElevation
     ) {
         val zIndex by animateFloatAsState(
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
index 1108c8c1..969886e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
@@ -56,7 +56,6 @@
  * still happen internally.
  * @param content content of the [Tab]
  */
-@ExperimentalTvMaterial3Api
 @Composable
 fun TabRowScope.Tab(
     selected: Boolean,
@@ -106,7 +105,6 @@
  * - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
  * using an Underlined indicator
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 class TabColors
 internal constructor(
     internal val contentColor: Color,
@@ -147,7 +145,6 @@
     }
 }
 
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 object TabDefaults {
     /**
      * [Tab]'s content colors to in conjunction with underlined indicator
@@ -163,7 +160,6 @@
      * focused
      * @param disabledSelectedContentColor applied when the current tab is disabled and selected
      */
-    @OptIn(ExperimentalTvMaterial3Api::class)
     @Composable
     fun underlinedIndicatorTabColors(
         contentColor: Color = LocalContentColor.current,
@@ -200,7 +196,6 @@
      * focused
      * @param disabledSelectedContentColor applied when the current tab is disabled and selected
      */
-    @OptIn(ExperimentalTvMaterial3Api::class)
     @Composable
     fun pillIndicatorTabColors(
         contentColor: Color = LocalContentColor.current,
@@ -224,7 +219,6 @@
         )
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 internal fun TabColors.toToggleableSurfaceColors(
     doesTabRowHaveFocus: Boolean,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
index 322f2aa..ed819d3 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
@@ -86,7 +86,6 @@
  * * doesTabRowHaveFocus: whether any [Tab] within [TabRow] is focused
  * @param tabs a composable which will render all the tabs
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 @Composable
 fun TabRow(
     selectedTabIndex: Int,
@@ -190,7 +189,6 @@
     }
 }
 
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 object TabRowDefaults {
     /** Color of the background of a tab */
     val ContainerColor = Color.Transparent
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/TabRowScope.kt b/tv/tv-material/src/main/java/androidx/tv/material3/TabRowScope.kt
index 27b9848..e98c590 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/TabRowScope.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/TabRowScope.kt
@@ -19,7 +19,6 @@
 /**
  * [TabRowScope] is used to provide the doesTabRowHaveFocus state to the [Tab] composable
  */
-@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 interface TabRowScope {
     /**
      * Whether any [Tab] within the [TabRow] is focused
@@ -28,7 +27,6 @@
     val hasFocus: Boolean
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal class TabRowScopeImpl internal constructor(
     override val hasFocus: Boolean
 ) : TabRowScope
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
index b773532..6ef08dc 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
@@ -80,13 +80,14 @@
  * @param maxLines an optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
  * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param onTextLayout callback that is executed when a new text layout is calculated. A
  * [TextLayoutResult] object that callback provides contains paragraph information, size of the
  * text, baselines and other details. The callback can be used to add additional decoration or
  * functionality to the text. For example, to draw selection around the text.
  * @param style style configuration for the text such as color, font, line height etc.
  */
-@ExperimentalTvMaterial3Api
 @Composable
 fun Text(
     text: String,
@@ -98,11 +99,12 @@
     fontFamily: FontFamily? = null,
     letterSpacing: TextUnit = TextUnit.Unspecified,
     textDecoration: TextDecoration? = null,
-    textAlign: TextAlign = TextAlign.Unspecified,
+    textAlign: TextAlign? = null,
     lineHeight: TextUnit = TextUnit.Unspecified,
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
 ) {
@@ -120,7 +122,7 @@
                 color = textColor,
                 fontSize = fontSize,
                 fontWeight = fontWeight,
-                textAlign = textAlign,
+                textAlign = textAlign ?: TextAlign.Unspecified,
                 lineHeight = lineHeight,
                 fontFamily = fontFamily,
                 textDecoration = textDecoration,
@@ -130,7 +132,8 @@
         onTextLayout,
         overflow,
         softWrap,
-        maxLines
+        maxLines,
+        minLines
     )
 }
 
@@ -176,6 +179,8 @@
  * @param maxLines an optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
  * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param inlineContent a map storing composables that replaces certain ranges of the text, used to
  * insert composables into text layout. See [InlineTextContent].
  * @param onTextLayout callback that is executed when a new text layout is calculated. A
@@ -184,7 +189,6 @@
  * functionality to the text. For example, to draw selection around the text.
  * @param style style configuration for the text such as color, font, line height etc.
  */
-@ExperimentalTvMaterial3Api
 @Composable
 fun Text(
     text: AnnotatedString,
@@ -196,11 +200,12 @@
     fontFamily: FontFamily? = null,
     letterSpacing: TextUnit = TextUnit.Unspecified,
     textDecoration: TextDecoration? = null,
-    textAlign: TextAlign = TextAlign.Unspecified,
+    textAlign: TextAlign? = null,
     lineHeight: TextUnit = TextUnit.Unspecified,
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
     inlineContent: Map<String, InlineTextContent> = mapOf(),
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
@@ -218,7 +223,7 @@
                 color = textColor,
                 fontSize = fontSize,
                 fontWeight = fontWeight,
-                textAlign = textAlign,
+                textAlign = textAlign ?: TextAlign.Unspecified,
                 lineHeight = lineHeight,
                 fontFamily = fontFamily,
                 textDecoration = textDecoration,
@@ -229,6 +234,7 @@
         overflow = overflow,
         softWrap = softWrap,
         maxLines = maxLines,
+        minLines = minLines,
         inlineContent = inlineContent
     )
 }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
index e59da65..36ec04c 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
@@ -71,7 +71,6 @@
  * content
  * @param content the content of the button
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun WideButton(
@@ -144,7 +143,6 @@
  * @param contentPadding the spacing values to apply internally between the container and the
  * content
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun WideButton(
@@ -222,7 +220,6 @@
     }
 }
 
-@ExperimentalTvMaterial3Api
 @Composable
 private fun WideButtonImpl(
     onClick: () -> Unit,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/WideButtonDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/WideButtonDefaults.kt
index 07402ff..f4ca41c 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/WideButtonDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/WideButtonDefaults.kt
@@ -43,7 +43,6 @@
     val VerticalContentGap = 4.dp
 }
 
-@ExperimentalTvMaterial3Api
 object WideButtonDefaults {
     private val HorizontalPadding = 16.dp
     private val VerticalPadding = 10.dp
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt
index 7091c76..49873f8 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt
@@ -34,7 +34,7 @@
     val BodySmallFont = TypefaceTokens.Plain
     val BodySmallLineHeight = 16.0.sp
     val BodySmallSize = 12.sp
-    val BodySmallTracking = 0.4.sp
+    val BodySmallTracking = 0.2.sp
     val BodySmallWeight = TypefaceTokens.WeightRegular
     val DisplayLargeFont = TypefaceTokens.Brand
     val DisplayLargeLineHeight = 64.0.sp
diff --git a/tvprovider/tvprovider/api/current.txt b/tvprovider/tvprovider/api/current.txt
index 1ca7bb2..90bedf9 100644
--- a/tvprovider/tvprovider/api/current.txt
+++ b/tvprovider/tvprovider/api/current.txt
@@ -260,7 +260,7 @@
     method public androidx.tvprovider.media.tv.PreviewProgram.Builder! setWeight(int);
   }
 
-  public final class Program implements java.lang.Comparable<androidx.tvprovider.media.tv.Program> {
+  public final class Program implements java.lang.Comparable<androidx.tvprovider.media.tv.Program!> {
     method public int compareTo(androidx.tvprovider.media.tv.Program);
     method public static androidx.tvprovider.media.tv.Program! fromCursor(android.database.Cursor!);
     method public String![]! getAudioLanguages();
diff --git a/tvprovider/tvprovider/api/restricted_current.txt b/tvprovider/tvprovider/api/restricted_current.txt
index a52b03f..797e575 100644
--- a/tvprovider/tvprovider/api/restricted_current.txt
+++ b/tvprovider/tvprovider/api/restricted_current.txt
@@ -289,7 +289,7 @@
     method public androidx.tvprovider.media.tv.PreviewProgram.Builder! setWeight(int);
   }
 
-  public final class Program implements java.lang.Comparable<androidx.tvprovider.media.tv.Program> {
+  public final class Program implements java.lang.Comparable<androidx.tvprovider.media.tv.Program!> {
     method public int compareTo(androidx.tvprovider.media.tv.Program);
     method public static androidx.tvprovider.media.tv.Program! fromCursor(android.database.Cursor!);
     method public String![]! getAudioLanguages();
diff --git a/vectordrawable/vectordrawable-animated/api/restricted_current.txt b/vectordrawable/vectordrawable-animated/api/restricted_current.txt
index 502a7a7..7368567 100644
--- a/vectordrawable/vectordrawable-animated/api/restricted_current.txt
+++ b/vectordrawable/vectordrawable-animated/api/restricted_current.txt
@@ -42,7 +42,7 @@
     method public static android.animation.Animator! loadAnimator(android.content.Context!, @AnimatorRes int) throws android.content.res.Resources.NotFoundException;
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ArgbEvaluator implements android.animation.TypeEvaluator<java.lang.Object> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ArgbEvaluator implements android.animation.TypeEvaluator<java.lang.Object!> {
     ctor public ArgbEvaluator();
     method public Object! evaluate(float, Object!, Object!);
     method public static androidx.vectordrawable.graphics.drawable.ArgbEvaluator getInstance();
diff --git a/viewpager2/viewpager2/api/1.1.0-beta03.txt b/viewpager2/viewpager2/api/1.1.0-beta03.txt
index c3dd7a2..36c6eb5 100644
--- a/viewpager2/viewpager2/api/1.1.0-beta03.txt
+++ b/viewpager2/viewpager2/api/1.1.0-beta03.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.viewpager2.adapter {
 
-  public abstract class FragmentStateAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.viewpager2.adapter.FragmentViewHolder> implements androidx.viewpager2.adapter.StatefulAdapter {
+  public abstract class FragmentStateAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.viewpager2.adapter.FragmentViewHolder!> implements androidx.viewpager2.adapter.StatefulAdapter {
     ctor public FragmentStateAdapter(androidx.fragment.app.Fragment);
     ctor public FragmentStateAdapter(androidx.fragment.app.FragmentActivity);
     ctor public FragmentStateAdapter(androidx.fragment.app.FragmentManager, androidx.lifecycle.Lifecycle);
diff --git a/viewpager2/viewpager2/api/current.txt b/viewpager2/viewpager2/api/current.txt
index c3dd7a2..36c6eb5 100644
--- a/viewpager2/viewpager2/api/current.txt
+++ b/viewpager2/viewpager2/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.viewpager2.adapter {
 
-  public abstract class FragmentStateAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.viewpager2.adapter.FragmentViewHolder> implements androidx.viewpager2.adapter.StatefulAdapter {
+  public abstract class FragmentStateAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.viewpager2.adapter.FragmentViewHolder!> implements androidx.viewpager2.adapter.StatefulAdapter {
     ctor public FragmentStateAdapter(androidx.fragment.app.Fragment);
     ctor public FragmentStateAdapter(androidx.fragment.app.FragmentActivity);
     ctor public FragmentStateAdapter(androidx.fragment.app.FragmentManager, androidx.lifecycle.Lifecycle);
diff --git a/viewpager2/viewpager2/api/restricted_1.1.0-beta03.txt b/viewpager2/viewpager2/api/restricted_1.1.0-beta03.txt
index 38e9cd9..a98edba 100644
--- a/viewpager2/viewpager2/api/restricted_1.1.0-beta03.txt
+++ b/viewpager2/viewpager2/api/restricted_1.1.0-beta03.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.viewpager2.adapter {
 
-  public abstract class FragmentStateAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.viewpager2.adapter.FragmentViewHolder> implements androidx.viewpager2.adapter.StatefulAdapter {
+  public abstract class FragmentStateAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.viewpager2.adapter.FragmentViewHolder!> implements androidx.viewpager2.adapter.StatefulAdapter {
     ctor public FragmentStateAdapter(androidx.fragment.app.Fragment);
     ctor public FragmentStateAdapter(androidx.fragment.app.FragmentActivity);
     ctor public FragmentStateAdapter(androidx.fragment.app.FragmentManager, androidx.lifecycle.Lifecycle);
diff --git a/viewpager2/viewpager2/api/restricted_current.txt b/viewpager2/viewpager2/api/restricted_current.txt
index 38e9cd9..a98edba 100644
--- a/viewpager2/viewpager2/api/restricted_current.txt
+++ b/viewpager2/viewpager2/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.viewpager2.adapter {
 
-  public abstract class FragmentStateAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.viewpager2.adapter.FragmentViewHolder> implements androidx.viewpager2.adapter.StatefulAdapter {
+  public abstract class FragmentStateAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.viewpager2.adapter.FragmentViewHolder!> implements androidx.viewpager2.adapter.StatefulAdapter {
     ctor public FragmentStateAdapter(androidx.fragment.app.Fragment);
     ctor public FragmentStateAdapter(androidx.fragment.app.FragmentActivity);
     ctor public FragmentStateAdapter(androidx.fragment.app.FragmentManager, androidx.lifecycle.Lifecycle);
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedTestTagTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedTestTagTest.kt
new file mode 100644
index 0000000..de30899
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedTestTagTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.wear.compose.foundation
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import org.junit.Rule
+import org.junit.Test
+
+class CurvedTestTagTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun curvedBox_supports_testTag() {
+        rule.setContent {
+            CurvedLayout {
+                curvedBox(
+                    modifier = CurvedModifier
+                        .testTag(TEST_TAG)
+                ) {
+                    curvedComposable {}
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun curvedRow_supports_testTag() {
+        rule.setContent {
+            CurvedLayout {
+                curvedRow(
+                    modifier = CurvedModifier
+                        .testTag(TEST_TAG)
+                ) {
+                    curvedComposable {}
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun curvedColumn_supports_testTag() {
+        rule.setContent {
+            CurvedLayout {
+                curvedColumn(
+                    modifier = CurvedModifier
+                        .testTag(TEST_TAG)
+                ) {
+                    curvedComposable {}
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun curvedComposable_supports_testTag() {
+        rule.setContent {
+            CurvedLayout {
+                curvedComposable(
+                    modifier = CurvedModifier
+                        .testTag(TEST_TAG)
+                ) {}
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
index f781f3a..33312b2 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
@@ -16,11 +16,17 @@
 
 package androidx.wear.compose.foundation
 
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.semantics
 import kotlin.math.abs
 import kotlin.math.atan2
 import kotlin.math.min
@@ -68,6 +74,34 @@
     return histogram
 }
 
+/**
+ * Applies a tag to allow modified curved container element to be found in tests. This is similar to
+ * [Modifier.testTag], but specifically for curved containers.
+ *
+ * This is a convenience method for a [semantics] that sets [SemanticsPropertyReceiver.testTag].
+ * Currently, this supports basic assert operations operations only.
+ *
+ * @param tag The tag to apply to the curved container.
+ */
+public fun CurvedModifier.testTag(
+    tag: String
+) = this.then { child ->
+    TestTagWrapper(child, tag)
+}
+
+private class TestTagWrapper(
+    val child: CurvedChild,
+    val tag: String
+) : BaseCurvedChildWrapper(child) {
+
+    @Composable
+    override fun SubComposition() {
+        Box(modifier = Modifier.testTag(tag)) {
+            super.SubComposition()
+        }
+    }
+}
+
 internal fun checkSpy(dimensions: RadialDimensions, capturedInfo: CapturedInfo) =
     checkCurvedLayoutInfo(dimensions.asCurvedLayoutInfo(), capturedInfo.lastLayoutInfo!!)
 
diff --git a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ToggleButtonTest.kt b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ToggleButtonTest.kt
index d1c11d8..1c523f1 100644
--- a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ToggleButtonTest.kt
+++ b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ToggleButtonTest.kt
@@ -1122,6 +1122,7 @@
     shape = shape,
     toggleControlWidth = selectionControlWidth,
     toggleControlHeight = selectionControlHeight,
+    labelSpacerSize = 0.dp,
     ripple = EmptyIndication,
 )
 
@@ -1172,6 +1173,7 @@
     clickInteractionSource = clickInteractionSource,
     contentPadding = contentPadding,
     shape = shape,
+    labelSpacerSize = 0.dp,
     ripple = EmptyIndication,
 )
 
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
index 397cd10..792bd00 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
@@ -188,6 +188,7 @@
     shape: Shape,
     toggleControlWidth: Dp,
     toggleControlHeight: Dp,
+    labelSpacerSize: Dp,
     ripple: Indication
 ) {
     // One and only one of toggleControl and selectionControl should be provided.
@@ -226,7 +227,8 @@
         ToggleButtonIcon(content = icon)
         Labels(
             label = label,
-            secondaryLabel = secondaryLabel
+            secondaryLabel = secondaryLabel,
+            spacerSize = labelSpacerSize
         )
         Spacer(
             modifier = Modifier.size(
@@ -311,6 +313,7 @@
     clickInteractionSource: MutableInteractionSource?,
     contentPadding: PaddingValues,
     shape: Shape,
+    labelSpacerSize: Dp,
     ripple: Indication
 ) {
     val (startPadding, endPadding) = contentPadding.splitHorizontally()
@@ -341,6 +344,7 @@
             Labels(
                 label = label,
                 secondaryLabel = secondaryLabel,
+                spacerSize = labelSpacerSize
             )
             Spacer(
                 modifier = Modifier
@@ -375,7 +379,7 @@
 
         Box(
             modifier =
-                boxModifier
+            boxModifier
                 .fillMaxHeight()
                 .drawWithCache {
                     onDrawWithContent {
@@ -409,11 +413,13 @@
 @Composable
 private fun RowScope.Labels(
     label: @Composable RowScope.() -> Unit,
-    secondaryLabel: @Composable (RowScope.() -> Unit)?
+    secondaryLabel: @Composable (RowScope.() -> Unit)?,
+    spacerSize: Dp = 0.dp
 ) {
     Column(modifier = Modifier.weight(1.0f)) {
         Row(content = label)
         if (secondaryLabel != null) {
+            Spacer(modifier = Modifier.size(spacerSize))
             Row(content = secondaryLabel)
         }
     }
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ToggleChip.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ToggleChip.kt
index 3c52ce4..f0911f0 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ToggleChip.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ToggleChip.kt
@@ -158,6 +158,7 @@
     shape = shape,
     toggleControlHeight = TOGGLE_CONTROL_HEIGHT,
     toggleControlWidth = TOGGLE_CONTROL_WIDTH,
+    labelSpacerSize = 0.dp,
     ripple = rippleOrFallbackImplementation()
 )
 
@@ -276,6 +277,7 @@
     shape = shape,
     toggleControlHeight = TOGGLE_CONTROL_HEIGHT,
     toggleControlWidth = TOGGLE_CONTROL_WIDTH,
+    labelSpacerSize = 0.dp,
     ripple = rippleOrFallbackImplementation()
 )
 
@@ -402,6 +404,7 @@
     clickInteractionSource = clickInteractionSource,
     contentPadding = contentPadding,
     shape = shape,
+    labelSpacerSize = 0.dp,
     ripple = rippleOrFallbackImplementation()
 )
 
@@ -525,6 +528,7 @@
     clickInteractionSource = clickInteractionSource,
     contentPadding = contentPadding,
     shape = shape,
+    labelSpacerSize = 0.dp,
     ripple = rippleOrFallbackImplementation()
 )
 
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/TouchExplorationStateProvider.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/TouchExplorationStateProvider.kt
index 7e3e7444..665daf1 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/TouchExplorationStateProvider.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/TouchExplorationStateProvider.kt
@@ -28,9 +28,9 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 /**
  * A functional interface for providing the state of touch exploration services. It is strongly
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 daf7691..2dc5958 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
@@ -782,7 +782,8 @@
     if (label != null) {
         ButtonImpl(
             onClick = onClick,
-            modifier = modifier.compactButtonModifier()
+            modifier = modifier
+                .compactButtonModifier()
                 .padding(ButtonDefaults.CompactButtonTapTargetPadding),
             secondaryLabel = null,
             icon = icon,
@@ -801,7 +802,8 @@
         // content. We use the base simple single slot Button under the covers.
         ButtonImpl(
             onClick = onClick,
-            modifier = modifier.compactButtonModifier()
+            modifier = modifier
+                .compactButtonModifier()
                 .width(ButtonDefaults.IconOnlyCompactButtonWidth)
                 .padding(ButtonDefaults.CompactButtonTapTargetPadding),
             enabled = enabled,
@@ -1451,7 +1453,8 @@
 
 @Composable
 private fun Modifier.buttonSizeModifier(): Modifier =
-    this.defaultMinSize(minHeight = ButtonDefaults.Height)
+    this
+        .defaultMinSize(minHeight = ButtonDefaults.Height)
         .height(IntrinsicSize.Min)
 
 @Composable
@@ -1557,13 +1560,14 @@
                     )
                 )
                 if (secondaryLabel != null && secondaryLabelFont != null) {
-                   Row(
-                       content = provideScopeContent(
-                           colors.secondaryContentColor(enabled),
-                           secondaryLabelFont,
-                           secondaryLabel
-                       )
-                   )
+                    Spacer(modifier = Modifier.size(2.dp))
+                    Row(
+                        content = provideScopeContent(
+                            colors.secondaryContentColor(enabled),
+                            secondaryLabelFont,
+                            secondaryLabel
+                        )
+                    )
                 }
             }
         }
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
index be6073c..70dbab71 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
@@ -500,6 +500,7 @@
             disabledUnselectedSplitContainerColor = disabledUnselectedSplitContainerColor
         )
 
+    internal val LabelSpacerSize = 2.dp
     private val HorizontalPadding = 14.dp
     private val VerticalPadding = 6.dp
 
@@ -1084,6 +1085,7 @@
     Column(modifier = Modifier.weight(1.0f)) {
         Row(content = label)
         if (secondaryLabel != null) {
+            Spacer(modifier = Modifier.size(RadioButtonDefaults.LabelSpacerSize))
             Row(content = secondaryLabel)
         }
     }
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
index 5155500..f88b805 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
@@ -242,8 +242,8 @@
 }
 
 private val RippleAlpha: RippleAlpha = RippleAlpha(
-    pressedAlpha = 0.12f,
-    focusedAlpha = 0.12f,
+    pressedAlpha = 0.10f,
+    focusedAlpha = 0.10f,
     draggedAlpha = 0.16f,
     hoveredAlpha = 0.08f
 )
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt
index 3436d0a..f48561e 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt
@@ -142,6 +142,7 @@
         shape = shape,
         toggleControlWidth = TOGGLE_CONTROL_WIDTH,
         toggleControlHeight = TOGGLE_CONTROL_HEIGHT,
+        labelSpacerSize = ToggleButtonDefaults.LabelSpacerSize,
         ripple = rippleOrFallbackImplementation()
     )
 
@@ -259,6 +260,7 @@
     clickInteractionSource = clickInteractionSource,
     contentPadding = contentPadding,
     shape = shape,
+    labelSpacerSize = ToggleButtonDefaults.LabelSpacerSize,
     ripple = rippleOrFallbackImplementation()
 )
 
@@ -425,17 +427,18 @@
             disabledUncheckedSplitContainerColor = disabledUncheckedSplitContainerColor
         )
 
-    private val ChipHorizontalPadding = 14.dp
-    private val ChipVerticalPadding = 6.dp
+    internal val LabelSpacerSize = 2.dp
+    private val HorizontalPadding = 14.dp
+    private val VerticalPadding = 6.dp
 
     /**
      * The default content padding used by [ToggleButton]
      */
     val ContentPadding: PaddingValues = PaddingValues(
-        start = ChipHorizontalPadding,
-        top = ChipVerticalPadding,
-        end = ChipHorizontalPadding,
-        bottom = ChipVerticalPadding
+        start = HorizontalPadding,
+        top = VerticalPadding,
+        end = HorizontalPadding,
+        bottom = VerticalPadding
     )
 
     private val ColorScheme.defaultToggleButtonColors: ToggleButtonColors
diff --git a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
index 360df14..64a5666 100644
--- a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
+++ b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
@@ -19,6 +19,7 @@
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
@@ -94,7 +95,7 @@
 
         // Click to move to next destination then swipe to dismiss.
         rule.onNodeWithText(START).performClick()
-        rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
 
         // Should now display "start".
         rule.onNodeWithText(START).assertExists()
@@ -195,23 +196,28 @@
                     composable(START) {
                         screenId.value = START
                         var toggle by rememberSaveable { mutableStateOf(false) }
-                        Column {
-                            ToggleButton(
-                                checked = toggle,
-                                onCheckedChange = {
-                                    toggle = !toggle
-                                },
-                                content = { Text(text = if (toggle) "On" else "Off") },
-                                modifier = Modifier.testTag("ToggleButton"),
-                            )
-                            Button(
-                                onClick = { navController.navigate(NEXT) },
-                            ) {
-                                Text("Go")
+                        Box(
+                            modifier = Modifier.fillMaxSize(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Column {
+                                ToggleButton(
+                                    checked = toggle,
+                                    onCheckedChange = {
+                                        toggle = !toggle
+                                    },
+                                    content = { Text(text = if (toggle) "On" else "Off") },
+                                    modifier = Modifier.testTag("ToggleButton"),
+                                )
+                                Button(
+                                    onClick = { navController.navigate(NEXT) },
+                                ) {
+                                    Text("Go")
+                                }
                             }
                         }
                     }
-                    composable("next") {
+                    composable(NEXT) {
                         screenId.value = NEXT
                         CompactChip(
                             onClick = {},
@@ -246,7 +252,9 @@
                         holder.SaveableStateProvider(START) {
                             var toggle by rememberSaveable { mutableStateOf(false) }
                             Column(
-                                modifier = Modifier.fillMaxSize().padding(horizontal = 20.dp),
+                                modifier = Modifier
+                                    .fillMaxSize()
+                                    .padding(horizontal = 20.dp),
                                 verticalArrangement = Arrangement.Center,
                                 horizontalAlignment = Alignment.CenterHorizontally
                             ) {
@@ -271,7 +279,9 @@
                         holder.SaveableStateProvider(NEXT) {
                             var counter by rememberSaveable { mutableStateOf(0) }
                             Column(
-                                modifier = Modifier.fillMaxSize().padding(horizontal = 20.dp),
+                                modifier = Modifier
+                                    .fillMaxSize()
+                                    .padding(horizontal = 20.dp),
                                 verticalArrangement = Arrangement.Center,
                                 horizontalAlignment = Alignment.CenterHorizontally
                             ) {
@@ -448,13 +458,23 @@
             userSwipeEnabled = userSwipeEnabled
         ) {
             composable(START) {
-                CompactChip(
-                    onClick = { navController.navigate(NEXT) },
-                    label = { Text(text = START) }
-                )
+                Box(
+                    modifier = Modifier.fillMaxSize(),
+                    contentAlignment = Alignment.Center
+                ) {
+                    CompactChip(
+                        onClick = { navController.navigate(NEXT) },
+                        label = { Text(text = START) }
+                    )
+                }
             }
             composable("next") {
-                Text(NEXT)
+                Box(
+                    modifier = Modifier.fillMaxSize(),
+                    contentAlignment = Alignment.Center
+                ) {
+                    Text(NEXT)
+                }
             }
         }
     }
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
index 7a0c478..9a3ef1a 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
@@ -58,7 +58,6 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.semantics
@@ -67,6 +66,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.wear.compose.integration.demos.common.rsbScroll
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.Icon
diff --git a/wear/compose/integration-tests/macrobenchmark/build.gradle b/wear/compose/integration-tests/macrobenchmark/build.gradle
index dd9f82f..1b3a97c 100644
--- a/wear/compose/integration-tests/macrobenchmark/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark/build.gradle
@@ -27,8 +27,6 @@
     namespace "androidx.wear.compose.integration.macrobenchmark"
     targetProjectPath = ":wear:compose:integration-tests:macrobenchmark-target"
     experimentalProperties["android.experimental.self-instrumenting"] = true
-
-    testOptions.animationsDisabled = false
 }
 
 // Create a release build type and make sure it's the only one enabled.
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/CompositionMetric.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/CompositionMetric.kt
index 784061a..5ea1fc5 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/CompositionMetric.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/CompositionMetric.kt
@@ -25,7 +25,7 @@
 @OptIn(ExperimentalMetricApi::class)
 internal class CompositionMetric(private val composable: String) : TraceMetric() {
     @OptIn(ExperimentalMetricApi::class, ExperimentalPerfettoTraceProcessorApi::class)
-    override fun getResult(
+    override fun getMeasurements(
         captureInfo: CaptureInfo,
         traceSession: PerfettoTraceProcessor.Session
     ): List<Measurement> {
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
index 7a52322..dce428c 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
@@ -66,7 +66,12 @@
             // Setting a gesture margin is important otherwise gesture nav is triggered.
             swipeToDismissBox.setGestureMargin(device.displayWidth / 5)
             repeat(3) {
-                swipeToDismissBox.swipe(Direction.RIGHT, 0.75f, SWIPE_SPEED)
+                swipeToDismissBox.swipe(Direction.RIGHT, 1f, SWIPE_SPEED)
+                // Sleeping the current thread for sometime before swiping again. This is required
+                // for cuttlefish_wear emulator as swipes are not completed when performed
+                // repeatedly. See b/328016250 for more details.
+                // TODO(b/329837878): Remove the sleep once infra improves
+                Thread.sleep(500)
                 device.waitForIdle()
             }
         }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
index 72e0f87..b467aa7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
@@ -54,6 +54,11 @@
         @Override
         @UiThread
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic boolean node that gets value from the state. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
index b0babff..3a1cc92 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
@@ -64,4 +64,13 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @VisibleForTesting
     int getDynamicNodeCount();
+
+    /**
+     * Returns the cost of dynamic nodes that this dynamic type contains. See {@link
+     * DynamicDataNode#getCost()} for more details on node cost.
+     */
+    @UiThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @VisibleForTesting
+    int getDynamicNodeCost();
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
index 4481752..33a7b2e 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
@@ -84,6 +84,11 @@
     }
 
     @Override
+    public int getDynamicNodeCost() {
+        return mNodes.stream().mapToInt(DynamicDataNode::getCost).sum();
+    }
+
+    @Override
     public void close() {
         if (Looper.getMainLooper().isCurrentThread()) {
             closeInternal();
@@ -106,6 +111,6 @@
         mNodes.stream()
                 .filter(n -> n instanceof DynamicDataSourceNode)
                 .forEach(n -> ((DynamicDataSourceNode<?>) n).destroy());
-        mDynamicDataNodesQuotaManager.releaseQuota(getDynamicNodeCount());
+        mDynamicDataNodesQuotaManager.releaseQuota(getDynamicNodeCost());
     }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
index c2a1d58..afc44fe 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
@@ -35,8 +35,7 @@
         private final DynamicTypeValueReceiverWithPreUpdate<Integer> mDownstream;
 
         FixedColorNode(
-                FixedColor protoNode,
-                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
+                FixedColor protoNode, DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
             this.mValue = protoNode.getArgb();
             this.mDownstream = downstream;
         }
@@ -55,6 +54,11 @@
 
         @Override
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic color node that gets value from the platform source. */
@@ -120,6 +124,11 @@
         public void destroy() {
             mQuotaAwareAnimator.stopAnimator();
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic color node that gets animatable value from dynamic source. */
@@ -203,5 +212,10 @@
         public DynamicTypeValueReceiverWithPreUpdate<Integer> getInputCallback() {
             return mInputCallback;
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
index d0884d1..0817f67 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
@@ -190,4 +190,9 @@
             mDownstream.onData(mLastFalseValue);
         }
     }
+
+    @Override
+    public int getCost() {
+        return DEFAULT_NODE_COST;
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DurationNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DurationNodes.java
index d59ecea..92e907f 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DurationNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DurationNodes.java
@@ -54,6 +54,11 @@
 
         @Override
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic duration node that gets the duration between two time instants. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
index 5bcb602..41d8206 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
@@ -76,6 +76,11 @@
         this.mTransformer = transformer;
     }
 
+    @Override
+    public int getCost() {
+        return DEFAULT_NODE_COST;
+    }
+
     private class UpstreamCallback<T> implements DynamicTypeValueReceiverWithPreUpdate<T> {
         private boolean mUpstreamPreUpdated = false;
 
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
index d845acb..34e38f7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
@@ -16,6 +16,8 @@
 
 package androidx.wear.protolayout.expression.pipeline;
 
+import androidx.annotation.RestrictTo;
+
 /**
  * Node within a dynamic data pipeline.
  *
@@ -56,4 +58,17 @@
  *
  * @param <O> The data type that this node yields.
  */
-interface DynamicDataNode<O> {}
+interface DynamicDataNode<O> {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    int DEFAULT_NODE_COST = 1;
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    int FIXED_NODE_COST = 0;
+
+    /**
+     * Returns the cost of this node. This value is used to estimate performance impact of a node.
+     * By default, most nodes have a cost of {@link DynamicDataNode#DEFAULT_NODE_COST}.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    int getCost();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
index 441ee6d..1a8b456 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
@@ -28,8 +28,8 @@
 interface DynamicDataSourceNode<T> extends DynamicDataNode<T> {
     /**
      * Called on all source nodes before {@link DynamicDataSourceNode#init()} is called on any node.
-     * This should generally only call {@link
-     * DynamicTypeValueReceiverWithPreUpdate#onPreUpdate()} on all downstream nodes.
+     * This should generally only call {@link DynamicTypeValueReceiverWithPreUpdate#onPreUpdate()}
+     * on all downstream nodes.
      */
     @UiThread
     void preInit();
@@ -44,4 +44,9 @@
     /** Destroy this node. This should cause it to unbind from any data sources. */
     @UiThread
     void destroy();
+
+    @Override
+    default int getCost() {
+        return DEFAULT_NODE_COST;
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
index 160d4bc..0024b26 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
@@ -75,4 +75,9 @@
     public DynamicTypeValueReceiverWithPreUpdate<I> getIncomingCallback() {
         return mCallback;
     }
+
+    @Override
+    public int getCost() {
+        return DEFAULT_NODE_COST;
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 8e73af5..6229642 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -117,9 +117,9 @@
                 public boolean tryAcquireQuota(int quota) {
                     return true;
                 }
+
                 @Override
-                public void releaseQuota(int quota) {
-                }
+                public void releaseQuota(int quota) {}
             };
 
     @NonNull
@@ -389,7 +389,7 @@
     public BoundDynamicType bind(@NonNull DynamicTypeBindingRequest request)
             throws EvaluationException {
         BoundDynamicTypeImpl boundDynamicType = request.callBindOn(this);
-        if (!mDynamicTypesQuotaManager.tryAcquireQuota(boundDynamicType.getDynamicNodeCount())) {
+        if (!mDynamicTypesQuotaManager.tryAcquireQuota(boundDynamicType.getDynamicNodeCost())) {
             throw new EvaluationException(
                     "Dynamic type expression limit reached. Try making the dynamic type expression"
                             + " shorter or reduce the number of dynamic type expressions.");
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
index 333eec4..9ec6b85 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
@@ -64,6 +64,11 @@
         @Override
         @UiThread
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic float node that gets value from the state. */
@@ -183,6 +188,11 @@
         public void destroy() {
             mQuotaAwareAnimator.stopAnimator();
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic float node that gets animatable value from dynamic source. */
@@ -265,6 +275,11 @@
         public DynamicTypeValueReceiverWithPreUpdate<Float> getInputCallback() {
             return mInputCallback;
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     private static boolean isValid(Float value) {
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/InstantNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/InstantNodes.java
index c79546b..aa3a242 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/InstantNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/InstantNodes.java
@@ -53,6 +53,11 @@
 
         @Override
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic Instant node that gets value from the platform source. */
@@ -97,6 +102,11 @@
                 mEpochTimePlatformDataSource.unregisterForData(mDownstream);
             }
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic Instant node that gets value from the state. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
index 64e8513..e32fd70 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
@@ -75,6 +75,11 @@
         @Override
         @UiThread
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic integer node that gets value from the platform source. */
@@ -288,6 +293,11 @@
         public void destroy() {
             mQuotaAwareAnimator.stopAnimator();
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic int32 node that gets animatable value from dynamic source. */
@@ -370,6 +380,11 @@
         public DynamicTypeValueReceiverWithPreUpdate<Integer> getInputCallback() {
             return mInputCallback;
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic integer node that gets date-time part from a zoned date-time. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
index 98da705..d232317 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
@@ -104,4 +104,9 @@
 
         return new PlatformDataKey<T>(namespace, key);
     }
+
+    @Override
+    public int getCost() {
+        return DEFAULT_NODE_COST;
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
index 1978b04..fa65764 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
@@ -56,6 +56,11 @@
         @Override
         @UiThread
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic string node that gets a value from integer. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
index dca1d9e..0abb008 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
@@ -28,6 +28,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.expression.AppDataKey;
 import androidx.wear.protolayout.expression.DynamicBuilders;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool;
 import androidx.wear.protolayout.expression.PlatformDataKey;
@@ -54,7 +55,7 @@
         ArrayList<Boolean> results = new ArrayList<>();
         DynamicTypeBindingRequest request = createSingleNodeDynamicBoolRequest(results);
         BoundDynamicType boundDynamicType = evaluator.bind(request);
-        assertThat(boundDynamicType.getDynamicNodeCount()).isEqualTo(1);
+        assertThat(boundDynamicType.getDynamicNodeCost()).isEqualTo(1);
     }
 
     @Test
@@ -114,7 +115,7 @@
         boundDynamicType1.close();
         // Retry binding request2
         BoundDynamicType boundDynamicType2 = evaluator.bind(request2);
-        assertThat(boundDynamicType2.getDynamicNodeCount()).isEqualTo(1);
+        assertThat(boundDynamicType2.getDynamicNodeCost()).isEqualTo(1);
     }
 
     @Test
@@ -155,10 +156,14 @@
     @NonNull
     private static DynamicTypeBindingRequest createSingleNodeDynamicBoolRequest(
             ArrayList<Boolean> results) {
+        return createDynamicBoolRequest(DynamicBool.from(new AppDataKey<>("key")), results);
+    }
+
+    @NonNull
+    private static DynamicTypeBindingRequest createDynamicBoolRequest(
+            DynamicBuilders.DynamicBool dynamicBool, ArrayList<Boolean> results) {
         return DynamicTypeBindingRequest.forDynamicBool(
-                DynamicBool.constant(false),
-                new MainThreadExecutor(),
-                new AddToListCallback<>(results));
+                dynamicBool, new MainThreadExecutor(), new AddToListCallback<>(results));
     }
 
     @NonNull
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index f309942..bc3520f9 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -61,7 +61,7 @@
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setReverseRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
   }
 
-  public final class AppDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> extends androidx.wear.protolayout.expression.DynamicDataKey<T> {
+  public final class AppDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> extends androidx.wear.protolayout.expression.DynamicDataKey<T!> {
     ctor public AppDataKey(String);
   }
 
@@ -334,7 +334,7 @@
   @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD}) public @interface ExperimentalProtoLayoutExtensionApi {
   }
 
-  public final class PlatformDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> extends androidx.wear.protolayout.expression.DynamicDataKey<T> {
+  public final class PlatformDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> extends androidx.wear.protolayout.expression.DynamicDataKey<T!> {
     ctor public PlatformDataKey(String, String);
   }
 
@@ -389,7 +389,7 @@
   public final class VersionBuilders {
   }
 
-  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class VersionBuilders.VersionInfo implements java.lang.Comparable<androidx.wear.protolayout.expression.VersionBuilders.VersionInfo> {
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class VersionBuilders.VersionInfo implements java.lang.Comparable<androidx.wear.protolayout.expression.VersionBuilders.VersionInfo!> {
     method public int compareTo(androidx.wear.protolayout.expression.VersionBuilders.VersionInfo);
     method public int getMajor();
     method public int getMinor();
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index f309942..bc3520f9 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -61,7 +61,7 @@
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setReverseRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
   }
 
-  public final class AppDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> extends androidx.wear.protolayout.expression.DynamicDataKey<T> {
+  public final class AppDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> extends androidx.wear.protolayout.expression.DynamicDataKey<T!> {
     ctor public AppDataKey(String);
   }
 
@@ -334,7 +334,7 @@
   @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD}) public @interface ExperimentalProtoLayoutExtensionApi {
   }
 
-  public final class PlatformDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> extends androidx.wear.protolayout.expression.DynamicDataKey<T> {
+  public final class PlatformDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> extends androidx.wear.protolayout.expression.DynamicDataKey<T!> {
     ctor public PlatformDataKey(String, String);
   }
 
@@ -389,7 +389,7 @@
   public final class VersionBuilders {
   }
 
-  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class VersionBuilders.VersionInfo implements java.lang.Comparable<androidx.wear.protolayout.expression.VersionBuilders.VersionInfo> {
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class VersionBuilders.VersionInfo implements java.lang.Comparable<androidx.wear.protolayout.expression.VersionBuilders.VersionInfo!> {
     method public int compareTo(androidx.wear.protolayout.expression.VersionBuilders.VersionInfo);
     method public int getMajor();
     method public int getMinor();
diff --git a/wear/protolayout/protolayout-lint/build.gradle b/wear/protolayout/protolayout-lint/build.gradle
index dfb3ae5..32a2997 100644
--- a/wear/protolayout/protolayout-lint/build.gradle
+++ b/wear/protolayout/protolayout-lint/build.gradle
@@ -30,6 +30,7 @@
 
 dependencies {
     compileOnly(libs.androidLintApi)
+    compileOnly(libs.androidLintChecks)
     compileOnly(libs.kotlinStdlib)
 
     testImplementation(libs.kotlinStdlib)
diff --git a/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutIssueRegistry.kt b/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutIssueRegistry.kt
index d47fe85..44e2353 100644
--- a/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutIssueRegistry.kt
+++ b/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutIssueRegistry.kt
@@ -25,7 +25,12 @@
 class ProtoLayoutIssueRegistry : IssueRegistry() {
     override val api = 14
     override val minApi = CURRENT_API
-    override val issues = listOf(ProtoLayoutMinSchemaDetector.ISSUE)
+    override val issues =
+        listOf(
+            ProtoLayoutMinSchemaDetector.ISSUE,
+            ResponsiveLayoutDetector.PRIMARY_LAYOUT_ISSUE,
+            ResponsiveLayoutDetector.EDGE_CONTENT_LAYOUT_ISSUE
+        )
     override val vendor =
         Vendor(
             feedbackUrl = "https://issuetracker.google.com/issues/new?component=1112273",
diff --git a/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ResponsiveLayoutDetector.kt b/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ResponsiveLayoutDetector.kt
new file mode 100644
index 0000000..aab7991
--- /dev/null
+++ b/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ResponsiveLayoutDetector.kt
@@ -0,0 +1,230 @@
+/*
+ * 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.wear.protolayout.lint
+
+import com.android.tools.lint.checks.DataFlowAnalyzer
+import com.android.tools.lint.client.api.JavaEvaluator
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.ConstantEvaluator
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.getParentOfType
+import org.jetbrains.uast.skipParenthesizedExprUp
+
+private const val PRIMARY_LAYOUT_BUILDER =
+    "androidx.wear.protolayout.material.layouts.PrimaryLayout.Builder"
+
+private const val EDGE_CONTENT_LAYOUT_BUILDER =
+    "androidx.wear.protolayout.material.layouts.EdgeContentLayout.Builder"
+
+private const val RESPONSIVE_SETTER_NAME = "setResponsiveContentInsetEnabled"
+
+// TODO(b/328785945): Improve edge cases like different scope for calls.
+class ResponsiveLayoutDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableConstructorTypes(): List<String> =
+        listOf(PRIMARY_LAYOUT_BUILDER, EDGE_CONTENT_LAYOUT_BUILDER)
+
+    override fun visitConstructor(
+        context: JavaContext,
+        node: UCallExpression,
+        constructor: PsiMethod
+    ) {
+        val containingClass = constructor.containingClass
+        val evaluator = context.evaluator
+        var issue: Issue? = null
+        var message = ""
+        var quickFix: LintFix? = null
+
+        if (isPrimaryLayout(evaluator, containingClass)) {
+            val potentialQuickFix = tryFindResponsiveSetter(context, node)
+            if (potentialQuickFix != null) {
+                issue = PRIMARY_LAYOUT_ISSUE
+                message =
+                    """
+                        PrimaryLayout used, but responsiveness isn't set: Please call
+                        `$RESPONSIVE_SETTER_NAME(true)` for the best results across
+                        different screen sizes.
+                    """
+                        .trimIndent()
+                quickFix = potentialQuickFix
+            }
+        }
+
+        if (isEdgeContentLayout(evaluator, containingClass)) {
+            val potentialQuickFix = tryFindResponsiveSetter(context, node)
+            if (potentialQuickFix != null) {
+                issue = EDGE_CONTENT_LAYOUT_ISSUE
+                message =
+                    """
+                            EdgeContentLayout used, but responsiveness isn't set: Please call
+                            `$RESPONSIVE_SETTER_NAME(true)` for the best results across
+                            different screen sizes and update to the looks of layout.
+                        """
+                        .trimIndent()
+                quickFix = potentialQuickFix
+            }
+        }
+
+        if (issue != null) {
+            context.report(
+                issue,
+                node,
+                context.getCallLocation(node, includeReceiver = true, includeArguments = false),
+                message,
+                quickFix,
+            )
+        }
+    }
+
+    private fun isEdgeContentLayout(evaluator: JavaEvaluator, containingClass: PsiClass?) =
+        evaluator.extendsClass(containingClass, EDGE_CONTENT_LAYOUT_BUILDER)
+
+    private fun isPrimaryLayout(evaluator: JavaEvaluator, containingClass: PsiClass?) =
+        evaluator.extendsClass(containingClass, PRIMARY_LAYOUT_BUILDER)
+
+    /** Tries to find a {@link #RESPONSIVE_SETTER_NAME} and provides a quick fix if not found. */
+    private fun tryFindResponsiveSetter(context: JavaContext, node: UCallExpression): LintFix? {
+        var foundResponsive = false
+        var foundFalseResponsive: UCallExpression? = null
+
+        val visitor =
+            object : DataFlowAnalyzer(setOf(node)) {
+                override fun receiver(call: UCallExpression) {
+                    if (call.methodName != RESPONSIVE_SETTER_NAME) {
+                        return
+                    }
+
+                    if (call.valueArgumentCount != 1) {
+                        return
+                    }
+
+                    val argValue = ConstantEvaluator.evaluate(context, call.valueArguments[0])
+
+                    if (argValue is Boolean && argValue) {
+                        // Found, everything is correct for now.
+                        foundResponsive = true
+                    } else {
+                        if (foundResponsive) {
+                            // We found a later call that called it with false, so the true
+                            // version will be overridden, which should still report.
+                            foundResponsive = false
+                        }
+                        // Since we found the wrong one, we need to provide a quick fix with
+                        // replacement value.
+                        foundFalseResponsive = call
+                    }
+                }
+            }
+
+        // Find a method/class/file (kt) where this expression is defined and visit all calls
+        // to see if responsiveness is called.
+        (node.getParentOfType(UCallExpression::class.java)
+                ?: node.getParentOfType(UMethod::class.java)
+                ?: node.getParentOfType(UClass::class.java)
+                ?: skipParenthesizedExprUp(node.uastParent))
+            ?.accept(visitor)
+
+        if (foundResponsive && foundFalseResponsive == null) {
+            return null
+        }
+
+        return if (foundFalseResponsive == null)
+            fix()
+                .replace()
+                .name("Call $RESPONSIVE_SETTER_NAME(true) on layouts")
+                .range(context.getLocation(node))
+                .end()
+                .with(".$RESPONSIVE_SETTER_NAME(true)")
+                .build()
+        else
+            fix()
+                .replace()
+                .name("Call $RESPONSIVE_SETTER_NAME(true) on layouts")
+                .range(
+                    context.getCallLocation(
+                        foundFalseResponsive!!,
+                        includeReceiver = false,
+                        includeArguments = true
+                    )
+                )
+                .pattern("(.*)")
+                .with("$RESPONSIVE_SETTER_NAME(true)")
+                .reformat(true)
+                .build()
+    }
+
+    companion object {
+        @JvmField
+        val PRIMARY_LAYOUT_ISSUE =
+            Issue.create(
+                id = "ProtoLayoutPrimaryLayoutResponsive",
+                briefDescription =
+                    "ProtoLayout Material PrimaryLayout should be used with responsive behaviour" +
+                        "to ensure the best behaviour across different screen sizes and locales.",
+                explanation =
+                    """
+            It is highly recommended to use the latest setResponsiveInsetEnabled(true) when you're
+            using the ProtoLayout's PrimaryLayout.
+
+            This is will take care of all inner padding to ensure that content of labels and bottom
+            chip doesn't go off the screen (especially with different locales).
+            """,
+                category = Category.CORRECTNESS,
+                priority = 5,
+                severity = Severity.WARNING,
+                androidSpecific = true,
+                implementation =
+                    Implementation(ResponsiveLayoutDetector::class.java, Scope.JAVA_FILE_SCOPE),
+            )
+
+        @JvmField
+        val EDGE_CONTENT_LAYOUT_ISSUE =
+            Issue.create(
+                id = "ProtoLayoutEdgeContentLayoutResponsive",
+                briefDescription =
+                    "ProtoLayout Material EdgeContentLayout should be used with responsive" +
+                        "behaviour to ensure the best behaviour across different screen sizes and" +
+                        "locales.",
+                explanation =
+                    """
+            It is highly recommended to use the latest setResponsiveInsetEnabled(true) when you're
+            using the ProtoLayout's EdgeContentLayout.
+
+            This is will take care of all outer margins and inner padding to ensure that content of
+            labels doesn't go off the screen (especially with different locales) and that primary
+            label is placed in the consistent place.
+            """,
+                category = Category.CORRECTNESS,
+                priority = 5,
+                severity = Severity.WARNING,
+                androidSpecific = true,
+                implementation =
+                    Implementation(ResponsiveLayoutDetector::class.java, Scope.JAVA_FILE_SCOPE),
+            )
+    }
+}
diff --git a/wear/protolayout/protolayout-lint/src/test/java/EdgeContentLayoutResponsiveDetectorTest.kt b/wear/protolayout/protolayout-lint/src/test/java/EdgeContentLayoutResponsiveDetectorTest.kt
new file mode 100644
index 0000000..a3f13a4
--- /dev/null
+++ b/wear/protolayout/protolayout-lint/src/test/java/EdgeContentLayoutResponsiveDetectorTest.kt
@@ -0,0 +1,483 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.wear.protolayout.lint
+
+import androidx.wear.protolayout.lint.ResponsiveLayoutDetector.Companion.EDGE_CONTENT_LAYOUT_ISSUE
+import androidx.wear.protolayout.lint.ResponsiveLayoutDetector.Companion.PRIMARY_LAYOUT_ISSUE
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class EdgeContentLayoutResponsiveDetectorTest : LintDetectorTest() {
+    override fun getDetector() = ResponsiveLayoutDetector()
+
+    override fun getIssues() = mutableListOf(PRIMARY_LAYOUT_ISSUE, EDGE_CONTENT_LAYOUT_ISSUE)
+
+    private val deviceParametersStub =
+        java(
+            """
+                package androidx.wear.protolayout;
+                public class DeviceParameters {}
+            """
+                .trimIndent()
+        )
+
+    private val edgeContentLayoutStub =
+        java(
+            """
+                package androidx.wear.protolayout.material.layouts;
+
+                import androidx.wear.protolayout.DeviceParameters;
+
+                public class EdgeContentLayout {
+                    public static class Builder {
+                        public Builder(DeviceParameters deviceParameters) {}
+                        public Builder() {}
+
+                        public Builder setResponsiveContentInsetEnabled(boolean enabled) {
+                            return this;
+                        }
+
+                        public EdgeContentLayout build() {
+                            return new EdgeContentLayout();
+                        }
+                    }
+                }
+            """
+                .trimIndent()
+        )
+
+    @Test
+    fun `edgeContentLayout with responsiveness doesn't report`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                edgeContentLayoutStub,
+                kotlin(
+                    """
+                        package foo
+                        import androidx.wear.protolayout.material.layouts.EdgeContentLayout
+
+                        val layout = EdgeContentLayout.Builder(null)
+                                .setResponsiveContentInsetEnabled(true)
+                                .build()
+
+                        class Bar {
+                         val layout = EdgeContentLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(true)
+                                    .build()
+
+                            fun build() {
+                                val l = EdgeContentLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(true)
+                                return l.build()
+                            }
+
+                            fun update() {
+                                return EdgeContentLayout.Builder()
+                                .setResponsiveContentInsetEnabled(true)
+                            }
+
+                            fun build2() {
+                                update().build()
+                            }
+
+                            fun callRandom() {
+                              random(EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(true))
+                            }
+                            fun random(val l: EdgeContentLayout.Builder) {}
+
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(EDGE_CONTENT_LAYOUT_ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun `edgeContentLayout without responsiveness requires and fixes setter`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                edgeContentLayoutStub,
+                kotlin(
+                        """
+                        package foo
+                        import androidx.wear.protolayout.material.layouts.EdgeContentLayout
+
+                        val layout = EdgeContentLayout.Builder(null)
+                                .setResponsiveContentInsetEnabled(false)
+                                .build()
+
+                        class Bar {
+                         val layout = EdgeContentLayout.Builder(null)
+                                .build()
+
+                         val layoutFalse = EdgeContentLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(false)
+                                .build()
+
+                            fun buildFalse() {
+                                val l = EdgeContentLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(false)
+                                return l.build()
+                            }
+
+                            fun update() {
+                                val enabled = false
+                                EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(enabled)
+                            }
+
+                            fun build() {
+                                update().build()
+                            }
+
+                            fun build2() {
+                                return EdgeContentLayout.Builder().build()
+                            }
+
+                            fun doubleFalse() {
+                                EdgeContentLayout.Builder()
+                                    .setResponsiveContentInsetEnabled(true)
+                                    .setResponsiveContentInsetEnabled(false)
+                            }
+
+                            fun callRandom() {
+                              random(EdgeContentLayout.Builder())
+                            }
+                            fun random(val l: EdgeContentLayout.Builder) {}
+
+                            fun condition(val cond: Boolean) {
+                                val e = EdgeContentLayout.Builder()
+                                if (cond) {
+                                  e.setResponsiveContentInsetEnabled(false)
+                                } else {
+                                  e.setResponsiveContentInsetEnabled(true)
+                                }
+                            }
+                        }
+                    """
+                    )
+                    .indented()
+            )
+            // To confirm they are not mixed up.
+            .issues(EDGE_CONTENT_LAYOUT_ISSUE, PRIMARY_LAYOUT_ISSUE)
+            .run()
+            .expect(
+                """
+                    src/foo/Bar.kt:4: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                    val layout = EdgeContentLayout.Builder(null)
+                                 ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:9: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                     val layout = EdgeContentLayout.Builder(null)
+                                  ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:12: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                     val layoutFalse = EdgeContentLayout.Builder(null)
+                                       ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:17: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            val l = EdgeContentLayout.Builder(null)
+                                    ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:24: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(enabled)
+                            ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:32: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            return EdgeContentLayout.Builder().build()
+                                   ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:36: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            EdgeContentLayout.Builder()
+                            ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:42: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                          random(EdgeContentLayout.Builder())
+                                 ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:47: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            val e = EdgeContentLayout.Builder()
+                                    ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 9 warnings
+                """
+                    .trimIndent()
+            )
+            .expectFixDiffs(
+                """
+                    Fix for src/foo/Bar.kt line 4: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -5 +5
+                    -         .setResponsiveContentInsetEnabled(false)
+                    +         .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 9: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -9 +9
+                    -  val layout = EdgeContentLayout.Builder(null)
+                    +  val layout = EdgeContentLayout.Builder(null).setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 12: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -13 +13
+                    -             .setResponsiveContentInsetEnabled(false)
+                    +             .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 17: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -18 +18
+                    -             .setResponsiveContentInsetEnabled(false)
+                    +             .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 24: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -24 +24
+                    -         EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(enabled)
+                    +         EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 32: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -32 +32
+                    -         return EdgeContentLayout.Builder().build()
+                    +         return EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(true).build()
+                    Fix for src/foo/Bar.kt line 36: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -38 +38
+                    -             .setResponsiveContentInsetEnabled(false)
+                    +             .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 42: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -42 +42
+                    -       random(EdgeContentLayout.Builder())
+                    +       random(EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(true))
+                    Fix for src/foo/Bar.kt line 47: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -49 +49
+                    -           e.setResponsiveContentInsetEnabled(false)
+                    +           e.setResponsiveContentInsetEnabled(true)
+                """
+                    .trimIndent()
+            )
+    }
+
+    @Test
+    fun `edgeContentLayout with responsiveness doesn't report (Java)`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                edgeContentLayoutStub,
+                java(
+                    """
+                        package foo;
+                        import androidx.wear.protolayout.material.layouts.EdgeContentLayout;
+
+                        class Bar {
+                            EdgeContentLayout layout = new EdgeContentLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(true)
+                                    .build();
+
+                            EdgeContentLayout build() {
+                                EdgeContentLayout l = new EdgeContentLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(true);
+                                return l.build();
+                            }
+
+                            EdgeContentLayout update() {
+                                return new EdgeContentLayout.Builder()
+                                    .setResponsiveContentInsetEnabled(true);
+                            }
+
+                            EdgeContentLayout build2() {
+                                update().build();
+                            }
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(EDGE_CONTENT_LAYOUT_ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun `edgeContentLayout without responsiveness requires and fixes setter (Java)`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                edgeContentLayoutStub,
+                java(
+                    """
+                        package foo;
+                        import androidx.wear.protolayout.material.layouts.EdgeContentLayout;
+
+                        class Bar {
+                            EdgeContentLayout layout = new EdgeContentLayout.Builder(null)
+                                .build();
+
+                            EdgeContentLayout layoutFalse = new EdgeContentLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(false)
+                                .build();
+
+                            EdgeContentLayout buildFalse() {
+                                EdgeContentLayout l = new EdgeContentLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(false);
+                                return l.build();
+                            }
+
+                            void update() {
+                                boolean enabled = false;
+                                new EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(enabled);
+                            }
+
+                            void build() {
+                                update().build();
+                            }
+
+                            EdgeContentLayout build2() {
+                                return new EdgeContentLayout.Builder().build();
+                            }
+
+                            void doubleFalse() {
+                                new EdgeContentLayout.Builder()
+                                    .setResponsiveContentInsetEnabled(true)
+                                    .setResponsiveContentInsetEnabled(false);
+                            }
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(EDGE_CONTENT_LAYOUT_ISSUE)
+            .run()
+            .expect(
+                """
+                    src/foo/Bar.java:5: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                        EdgeContentLayout layout = new EdgeContentLayout.Builder(null)
+                                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:8: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                        EdgeContentLayout layoutFalse = new EdgeContentLayout.Builder(null)
+                                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:13: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            EdgeContentLayout l = new EdgeContentLayout.Builder(null)
+                                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:20: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            new EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(enabled);
+                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:28: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            return new EdgeContentLayout.Builder().build();
+                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:32: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                            new EdgeContentLayout.Builder()
+                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 6 warnings
+                """
+                    .trimIndent()
+            )
+            .expectFixDiffs(
+                """
+                    Fix for src/foo/Bar.java line 5: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -5 +5
+                    -     EdgeContentLayout layout = new EdgeContentLayout.Builder(null)
+                    +     EdgeContentLayout layout = new EdgeContentLayout.Builder(null).setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.java line 8: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -9 +9
+                    -             .setResponsiveContentInsetEnabled(false)
+                    +             .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.java line 13: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -14 +14
+                    -             .setResponsiveContentInsetEnabled(false);
+                    +             .setResponsiveContentInsetEnabled(true);
+                    Fix for src/foo/Bar.java line 20: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -20 +20
+                    -         new EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(enabled);
+                    +         new EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(true);
+                    Fix for src/foo/Bar.java line 28: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -28 +28
+                    -         return new EdgeContentLayout.Builder().build();
+                    +         return new EdgeContentLayout.Builder().setResponsiveContentInsetEnabled(true).build();
+                    Fix for src/foo/Bar.java line 32: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -34 +34
+                    -             .setResponsiveContentInsetEnabled(false);
+                    +             .setResponsiveContentInsetEnabled(true);
+                """
+                    .trimIndent()
+            )
+    }
+
+    @Test
+    fun `edgeContentLayout false report`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                edgeContentLayoutStub,
+                kotlin(
+                    """
+                        package foo
+                        import androidx.wear.protolayout.material.layouts.EdgeContentLayout
+
+
+                        fun doubleTrue() {
+                            EdgeContentLayout.Builder()
+                                .setResponsiveContentInsetEnabled(false)
+                                .setResponsiveContentInsetEnabled(true)
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(EDGE_CONTENT_LAYOUT_ISSUE)
+            .run()
+            .expect(
+                """
+                    src/foo/test.kt:6: Warning: EdgeContentLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes and update to the looks of layout. [ProtoLayoutEdgeContentLayoutResponsive]
+                        EdgeContentLayout.Builder()
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                """
+                    .trimIndent()
+            )
+            .expectFixDiffs(
+                """
+                    Fix for src/foo/test.kt line 6: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -7 +7
+                    -         .setResponsiveContentInsetEnabled(false)
+                    @@ -9 +8
+                    +         .setResponsiveContentInsetEnabled(true)
+                """
+                    .trimIndent()
+            )
+    }
+}
diff --git a/wear/protolayout/protolayout-lint/src/test/java/PrimaryLayoutResponsiveDetectorTest.kt b/wear/protolayout/protolayout-lint/src/test/java/PrimaryLayoutResponsiveDetectorTest.kt
new file mode 100644
index 0000000..7de1c93
--- /dev/null
+++ b/wear/protolayout/protolayout-lint/src/test/java/PrimaryLayoutResponsiveDetectorTest.kt
@@ -0,0 +1,471 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.wear.protolayout.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class PrimaryLayoutResponsiveDetectorTest : LintDetectorTest() {
+    override fun getDetector() = ResponsiveLayoutDetector()
+
+    override fun getIssues() = mutableListOf(ResponsiveLayoutDetector.PRIMARY_LAYOUT_ISSUE)
+
+    private val deviceParametersStub =
+        java(
+            """
+                package androidx.wear.protolayout;
+                public class DeviceParameters {}
+            """
+                .trimIndent()
+        )
+
+    private val primaryLayoutStub =
+        java(
+            """
+                package androidx.wear.protolayout.material.layouts;
+
+                import androidx.wear.protolayout.DeviceParameters;
+
+                public class PrimaryLayout {
+                    public static class Builder {
+                        public Builder(DeviceParameters deviceParameters) {}
+                        public Builder() {}
+
+                        public Builder setResponsiveContentInsetEnabled(boolean enabled) {
+                            return this;
+                        }
+
+                        public PrimaryLayout build() {
+                            return new PrimaryLayout();
+                        }
+                    }
+                }
+            """
+                .trimIndent()
+        )
+
+    @Test
+    fun `primaryLayout with responsiveness doesn't report`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                primaryLayoutStub,
+                kotlin(
+                    """
+                        package foo
+                        import androidx.wear.protolayout.material.layouts.PrimaryLayout
+
+                        val layout = PrimaryLayout.Builder(null)
+                                .setResponsiveContentInsetEnabled(true)
+                                .build()
+
+                        class Bar {
+                         val layout = PrimaryLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(true)
+                                    .build()
+
+                            fun build() {
+                                val l = PrimaryLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(true)
+                                return l.build()
+                            }
+
+                            fun update() {
+                                return PrimaryLayout.Builder()
+                                .setResponsiveContentInsetEnabled(true)
+                            }
+
+                            fun build2() {
+                                update().build()
+                            }
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(ResponsiveLayoutDetector.PRIMARY_LAYOUT_ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun `primaryLayout without responsiveness requires and fixes setter`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                primaryLayoutStub,
+                kotlin(
+                        """
+                        package foo
+                        import androidx.wear.protolayout.material.layouts.PrimaryLayout
+
+                        val layout = PrimaryLayout.Builder(null)
+                                .setResponsiveContentInsetEnabled(false)
+                                .build()
+
+                        class Bar {
+                         val layout = PrimaryLayout.Builder(null)
+                                .build()
+
+                         val layoutFalse = PrimaryLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(false)
+                                .build()
+
+                            fun buildFalse() {
+                                val l = PrimaryLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(false)
+                                return l.build()
+                            }
+
+                            fun update() {
+                                val enabled = false
+                                PrimaryLayout.Builder().setResponsiveContentInsetEnabled(enabled)
+                            }
+
+                            fun build() {
+                                update().build()
+                            }
+
+                            fun build2() {
+                                return PrimaryLayout.Builder().build()
+                            }
+
+                            fun doubleFalse() {
+                                PrimaryLayout.Builder()
+                                    .setResponsiveContentInsetEnabled(true)
+                                    .setResponsiveContentInsetEnabled(false)
+                            }
+                        }
+                    """
+                    )
+                    .indented()
+            )
+            .issues(ResponsiveLayoutDetector.PRIMARY_LAYOUT_ISSUE)
+            .run()
+            .expect(
+                """
+                    src/foo/Bar.kt:4: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                    val layout = PrimaryLayout.Builder(null)
+                                 ~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:9: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                     val layout = PrimaryLayout.Builder(null)
+                                  ~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:12: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                     val layoutFalse = PrimaryLayout.Builder(null)
+                                       ~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:17: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                            val l = PrimaryLayout.Builder(null)
+                                    ~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:24: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                            PrimaryLayout.Builder().setResponsiveContentInsetEnabled(enabled)
+                            ~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:32: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                            return PrimaryLayout.Builder().build()
+                                   ~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.kt:36: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                            PrimaryLayout.Builder()
+                            ~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 7 warnings
+                """
+                    .trimIndent()
+            )
+            .expectFixDiffs(
+                """
+                    Fix for src/foo/Bar.kt line 4: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -5 +5
+                    -         .setResponsiveContentInsetEnabled(false)
+                    +         .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 9: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -9 +9
+                    -  val layout = PrimaryLayout.Builder(null)
+                    +  val layout = PrimaryLayout.Builder(null).setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 12: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -13 +13
+                    -             .setResponsiveContentInsetEnabled(false)
+                    +             .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 17: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -18 +18
+                    -             .setResponsiveContentInsetEnabled(false)
+                    +             .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 24: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -24 +24
+                    -         PrimaryLayout.Builder().setResponsiveContentInsetEnabled(enabled)
+                    +         PrimaryLayout.Builder().setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.kt line 32: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -32 +32
+                    -         return PrimaryLayout.Builder().build()
+                    +         return PrimaryLayout.Builder().setResponsiveContentInsetEnabled(true).build()
+                    Fix for src/foo/Bar.kt line 36: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -38 +38
+                    -             .setResponsiveContentInsetEnabled(false)
+                    +             .setResponsiveContentInsetEnabled(true)
+                """
+                    .trimIndent()
+            )
+    }
+
+    @Test
+    fun `primaryLayout with responsiveness doesn't (Java) report`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                primaryLayoutStub,
+                java(
+                    """
+                        package foo;
+                        import androidx.wear.protolayout.material.layouts.PrimaryLayout;
+
+                        class Bar {
+                            PrimaryLayout layout = new PrimaryLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(true)
+                                    .build();
+
+                            PrimaryLayout build() {
+                                PrimaryLayout l = new PrimaryLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(true);
+                                return l.build();
+                            }
+
+                            PrimaryLayout update() {
+                                return new PrimaryLayout.Builder()
+                                    .setResponsiveContentInsetEnabled(true);
+                            }
+
+                            PrimaryLayout build2() {
+                                update().build();
+                            }
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(ResponsiveLayoutDetector.PRIMARY_LAYOUT_ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun `primaryLayout without responsiveness requires and fixes setter (Java)`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                primaryLayoutStub,
+                java(
+                    """
+                        package foo;
+                        import androidx.wear.protolayout.material.layouts.PrimaryLayout;
+
+                        class Bar {
+                            PrimaryLayout layout = new PrimaryLayout.Builder(null)
+                                .build();
+
+                            PrimaryLayout layoutFalse = new PrimaryLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(false)
+                                .build();
+
+                            PrimaryLayout buildFalse() {
+                                PrimaryLayout l = new PrimaryLayout.Builder(null)
+                                    .setResponsiveContentInsetEnabled(false);
+                                return l.build();
+                            }
+
+                            void update() {
+                                boolean enabled = false;
+                                new PrimaryLayout.Builder().setResponsiveContentInsetEnabled(enabled);
+                            }
+
+                            void build() {
+                                update().build();
+                            }
+
+                            PrimaryLayout build2() {
+                                return new PrimaryLayout.Builder().build();
+                            }
+
+                            void doubleFalse() {
+                                new PrimaryLayout.Builder()
+                                    .setResponsiveContentInsetEnabled(true)
+                                    .setResponsiveContentInsetEnabled(false);
+                            }
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(ResponsiveLayoutDetector.PRIMARY_LAYOUT_ISSUE)
+            .run()
+            .expect(
+                """
+                    src/foo/Bar.java:5: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                        PrimaryLayout layout = new PrimaryLayout.Builder(null)
+                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:8: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                        PrimaryLayout layoutFalse = new PrimaryLayout.Builder(null)
+                                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:13: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                            PrimaryLayout l = new PrimaryLayout.Builder(null)
+                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:20: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                            new PrimaryLayout.Builder().setResponsiveContentInsetEnabled(enabled);
+                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:28: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                            return new PrimaryLayout.Builder().build();
+                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/foo/Bar.java:32: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                            new PrimaryLayout.Builder()
+                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 6 warnings
+                """
+                    .trimIndent()
+            )
+            .expectFixDiffs(
+                """
+                    Fix for src/foo/Bar.java line 5: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -5 +5
+                    -     PrimaryLayout layout = new PrimaryLayout.Builder(null)
+                    +     PrimaryLayout layout = new PrimaryLayout.Builder(null).setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.java line 8: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -9 +9
+                    -             .setResponsiveContentInsetEnabled(false)
+                    +             .setResponsiveContentInsetEnabled(true)
+                    Fix for src/foo/Bar.java line 13: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -14 +14
+                    -             .setResponsiveContentInsetEnabled(false);
+                    +             .setResponsiveContentInsetEnabled(true);
+                    Fix for src/foo/Bar.java line 20: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -20 +20
+                    -         new PrimaryLayout.Builder().setResponsiveContentInsetEnabled(enabled);
+                    +         new PrimaryLayout.Builder().setResponsiveContentInsetEnabled(true);
+                    Fix for src/foo/Bar.java line 28: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -28 +28
+                    -         return new PrimaryLayout.Builder().build();
+                    +         return new PrimaryLayout.Builder().setResponsiveContentInsetEnabled(true).build();
+                    Fix for src/foo/Bar.java line 32: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -34 +34
+                    -             .setResponsiveContentInsetEnabled(false);
+                    +             .setResponsiveContentInsetEnabled(true);
+                """
+                    .trimIndent()
+            )
+    }
+
+    @Test
+    fun `primaryLayout with responsiveness false positive reports`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                primaryLayoutStub,
+                kotlin(
+                    """
+                        package foo
+                        import androidx.wear.protolayout.material.layouts.PrimaryLayout
+
+                        class Bar {
+                            fun create(): PrimaryLayout.Builder {
+                              return PrimaryLayout.Builder()
+                            }
+                            fun build() {
+                              create().setResponsiveContentInsetEnabled(true).build()
+                            }
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(ResponsiveLayoutDetector.PRIMARY_LAYOUT_ISSUE)
+            .run()
+            .expect(
+                """
+                    src/foo/Bar.kt:6: Warning: PrimaryLayout used, but responsiveness isn't set: Please call
+                    setResponsiveContentInsetEnabled(true) for the best results across
+                    different screen sizes. [ProtoLayoutPrimaryLayoutResponsive]
+                          return PrimaryLayout.Builder()
+                                 ~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                """
+                    .trimIndent()
+            )
+            .expectFixDiffs(
+                """
+                    Fix for src/foo/Bar.kt line 6: Call setResponsiveContentInsetEnabled(true) on layouts:
+                    @@ -6 +6
+                    -       return PrimaryLayout.Builder()
+                    +       return PrimaryLayout.Builder().setResponsiveContentInsetEnabled(true)
+                """
+                    .trimIndent()
+            )
+    }
+
+    @Test
+    fun `primaryLayout with responsiveness false doesn't report`() {
+        lint()
+            .files(
+                deviceParametersStub,
+                primaryLayoutStub,
+                kotlin(
+                    """
+                        package foo
+                        import androidx.wear.protolayout.material.layouts.PrimaryLayout
+
+                        class Bar {
+                            fun create(): PrimaryLayout.Builder {
+                              return PrimaryLayout.Builder().setResponsiveContentInsetEnabled(true)
+                            }
+                            fun build() {
+                              create().setResponsiveContentInsetEnabled(false).build()
+                            }
+                        }
+                    """
+                        .trimIndent()
+                )
+            )
+            .issues(ResponsiveLayoutDetector.PRIMARY_LAYOUT_ISSUE)
+            .run()
+            .expectClean()
+    }
+}
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index 8215bb4..1e5c30f 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -38,6 +38,9 @@
     implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
     implementation("androidx.annotation:annotation-experimental:1.4.0")
 
+    lintChecks(project(":wear:protolayout:protolayout-lint"))
+    lintPublish(project(":wear:protolayout:protolayout-lint"))
+
 
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testCore)
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/NodeInfo.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/NodeInfo.java
index 84a7180..d9b90e4 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/NodeInfo.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/NodeInfo.java
@@ -25,7 +25,6 @@
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 import androidx.collection.ArraySet;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.expression.pipeline.BoundDynamicType;
 import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest;
 import androidx.wear.protolayout.expression.pipeline.QuotaManager;
@@ -33,6 +32,7 @@
 import androidx.wear.protolayout.proto.ModifiersProto.AnimatedVisibility;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger.InnerCase;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.PositionIdTree.TreeNode;
 
 import java.util.ArrayList;
@@ -148,7 +148,9 @@
     @VisibleForTesting
     @SuppressWarnings("RestrictTo")
     int size() {
-        return mActiveBoundTypes.stream().mapToInt(BoundDynamicType::getDynamicNodeCount).sum();
+        return mActiveBoundTypes.stream()
+                .mapToInt(BoundDynamicType::getDynamicNodeCount)
+                .sum();
     }
 
     /** Play the animation with the given trigger type. */
@@ -241,10 +243,10 @@
                         + mResolvedAvds.stream().filter(avd -> avd.mDrawable.isRunning()).count());
     }
 
-    /** Returns how many expression nodes evaluated. */
+    /** Returns the cost of evaluated expression nodes. */
     @VisibleForTesting
-    public int getExpressionNodesCount() {
-        return mActiveBoundTypes.stream().mapToInt(BoundDynamicType::getDynamicNodeCount).sum();
+    public int getExpressionDynamicNodesCost() {
+        return mActiveBoundTypes.stream().mapToInt(BoundDynamicType::getDynamicNodeCost).sum();
     }
 
     /** Stores the {@link AnimatedVisibility} associated with this node. */
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
index 5579a88..8a933ec 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
@@ -40,7 +40,6 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.expression.PlatformDataKey;
 import androidx.wear.protolayout.expression.pipeline.BoundDynamicType;
 import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest;
@@ -65,6 +64,7 @@
 import androidx.wear.protolayout.proto.ModifiersProto.ExitTransition;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger;
 import androidx.wear.protolayout.proto.TypesProto.BoolProp;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.NodeInfo.ResolvedAvd;
 
 import com.google.common.collect.ImmutableList;
@@ -1161,11 +1161,11 @@
                         .sum();
     }
 
-    /** Returns How many dynamic data nodes exist in the pipeline. */
+    /** Returns the cost of nodes existing in the pipeline. */
     @VisibleForTesting
-    public int getDynamicExpressionsNodesCount() {
+    public int getDynamicExpressionsNodesCost() {
         return mPositionIdTree.getAllNodes().stream()
-                .mapToInt(NodeInfo::getExpressionNodesCount)
+                .mapToInt(NodeInfo::getExpressionDynamicNodesCost)
                 .sum();
     }
 
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
index befefb4..096c2aa 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
@@ -41,7 +41,6 @@
 import androidx.annotation.Nullable;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.expression.AppDataKey;
 import androidx.wear.protolayout.expression.DynamicBuilders;
 import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
@@ -73,7 +72,6 @@
 import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor;
 import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
 import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
-import androidx.wear.protolayout.expression.proto.FixedProto.FixedString;
 import androidx.wear.protolayout.proto.ColorProto.ColorProp;
 import androidx.wear.protolayout.proto.DimensionProto.DegreesProp;
 import androidx.wear.protolayout.proto.DimensionProto.DpProp;
@@ -85,6 +83,7 @@
 import androidx.wear.protolayout.proto.TriggerProto.OnVisibleTrigger;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger.InnerCase;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline.PipelineMaker;
 import androidx.wear.protolayout.renderer.inflater.DefaultAndroidSeekableAnimatedImageResourceByResIdResolver;
 import androidx.wear.protolayout.renderer.inflater.ResourceResolvers.ResourceAccessException;
@@ -841,7 +840,7 @@
         String staticValue = "static";
 
         AtomicReference<String> currentValue = new AtomicReference<>();
-        FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(/* quotaCap= */ 3);
+        FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(/* quotaCap= */ 2);
 
         ProtoLayoutDynamicDataPipeline pipeline =
                 new ProtoLayoutDynamicDataPipeline(
@@ -872,7 +871,7 @@
         makePipelineForDynamicString(
                 pipeline, dynamicString, staticValue, "posId", currentValue::set);
         pipeline.initNewLayout();
-        expect.that(pipeline.getDynamicExpressionsNodesCount()).isEqualTo(3);
+        expect.that(pipeline.getDynamicExpressionsNodesCost()).isEqualTo(2);
         // No quota left
         expect.that(quotaManager.getRemainingQuota()).isEqualTo(0);
         expect.that(currentValue.get()).isEqualTo(expectedOutput);
@@ -880,8 +879,6 @@
 
     @Test
     public void newLayout_noExpressionNodesQuota_useStaticData() {
-
-        String dynamicValue = "dynamic";
         String staticValue = "static";
         AtomicReference<String> currentValue = new AtomicReference<>();
         FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(/* quotaCap= */ 0);
@@ -894,7 +891,8 @@
 
         DynamicString dynamicString =
                 DynamicString.newBuilder()
-                        .setFixed(FixedString.newBuilder().setValue(dynamicValue).build())
+                        .setInt32FormatOp(
+                                Int32FormatOp.newBuilder().setInput(fixedDynamicInt32(1)).build())
                         .build();
 
         makePipelineForDynamicString(
@@ -909,7 +907,7 @@
     public void newLayout_removeNodeInfo_releaseQuota() {
 
         int quota = 8;
-        DynamicBool expressionWith4Nodes = buildBoolExpressionWithFixedNumberOfNodes(4);
+        DynamicBool expressionWith4Nodes = buildBoolExpressionWithFixedNumberOfNodes(5);
         FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(quota);
 
         ProtoLayoutDynamicDataPipeline pipeline =
@@ -945,8 +943,10 @@
         String nodeInfo2 = "posId2.1";
         String nodeInfo3 = "posId3.1";
         int quota = 8;
-        DynamicBool expressionWith5Nodes = buildBoolExpressionWithFixedNumberOfNodes(5);
-        DynamicBool expressionWith1Nodes = buildBoolExpressionWithFixedNumberOfNodes(1);
+        // Cost = 5
+        DynamicBool expressionWith5Nodes = buildBoolExpressionWithFixedNumberOfNodes(6);
+        // Cost = 1
+        DynamicBool expressionWith1Nodes = buildBoolExpressionWithFixedNumberOfNodes(2);
         FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(quota);
 
         ProtoLayoutDynamicDataPipeline pipeline =
@@ -956,7 +956,7 @@
                         new FixedQuotaManagerImpl(MAX_VALUE),
                         quotaManager);
 
-        // Adding an expressions with 5 dynamic nodes to nodeInfo1.
+        // Adding an expressions cost = 4 to nodeInfo1.
         makePipelineForDynamicBool(pipeline, expressionWith5Nodes, nodeInfo1);
         pipeline.initNewLayout();
 
@@ -975,11 +975,11 @@
 
         // Remove nodeInfo1 and add nodeInfo3. nodeInfo2 still in the pipeline.
         pipeline.mPositionIdTree.removeChildNodesFor(parentOfNode1);
-        // Adding an expressions with 1 dynamic node to nodeInfo3.
+        // Adding an expressions cost = 1 to nodeInfo3.
         makePipelineForDynamicBool(pipeline, expressionWith1Nodes, nodeInfo3);
 
         pipeline.initNewLayout();
-        // Now the pipeline will have a total expressionNodesCount of 6 = 5 + 1 nodeInfo2 (failed to
+        // Now the pipeline will have a total expression cost of 6 = 5 + 1 nodeInfo2 (failed to
         // bound previously) and nodeInfo3(new) should be able to bound
         expect.that(quotaManager.getRemainingQuota()).isEqualTo(2);
         expect.that(pipeline.mPositionIdTree.get(nodeInfo3).getFailedBindingRequest().size())
@@ -992,8 +992,11 @@
     public void newLayout_multipleBound_noEnoughDynamicNodesQuota_satisfyOnlyFewBounds() {
 
         int quota = 11;
-        DynamicBool expressionWith12Nodes = buildBoolExpressionWithFixedNumberOfNodes(12);
+        // Cost = 12
+        DynamicBool expressionWith12Nodes = buildBoolExpressionWithFixedNumberOfNodes(13);
+        // Cost = 3
         DynamicBool expressionWith4Nodes = buildBoolExpressionWithFixedNumberOfNodes(4);
+        // Cost = 0
         DynamicBool expressionWith1Nodes = buildBoolExpressionWithFixedNumberOfNodes(1);
 
         FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(quota);
@@ -1013,7 +1016,7 @@
 
         pipeline.initNewLayout();
 
-        // expressionWith12Nodes related BoundType should file to bind.
+        // expressionWith12Nodes related BoundType should fail to bind.
         expect.that(
                         pipeline.mPositionIdTree
                                 .findFirst((node) -> node.getPosId().equals("posId1.0"))
@@ -1062,6 +1065,10 @@
                 .build();
     }
 
+    /**
+     * If count is equal to 1, this returns a FixedBool. Otherwise, it returns a dynamic expression
+     * containing one FixedBool and (count-1) NotBoolOp nodes.
+     */
     private static DynamicBool buildBoolExpressionWithFixedNumberOfNodes(int count) {
         if (count < 1) {
             throw new IllegalArgumentException();
diff --git a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
index 4487440..6402ad1 100644
--- a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
+++ b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
@@ -39,6 +39,7 @@
 import androidx.work.inspection.WorkManagerInspectorProtocol.Response
 import androidx.work.inspection.WorkManagerInspectorProtocol.TrackWorkManagerResponse
 import androidx.work.inspection.WorkManagerInspectorProtocol.WorkAddedEvent
+import androidx.work.inspection.WorkManagerInspectorProtocol.WorkInfo
 import androidx.work.inspection.WorkManagerInspectorProtocol.WorkRemovedEvent
 import androidx.work.inspection.WorkManagerInspectorProtocol.WorkUpdatedEvent
 import java.util.UUID
@@ -89,10 +90,13 @@
                 val response = Response.newBuilder()
                     .setTrackWorkManager(TrackWorkManagerResponse.getDefaultInstance())
                     .build()
-                workManager
+                val allWorkSpecIdsLiveData = workManager
                     .workDatabase
                     .workSpecDao()
                     .getAllWorkSpecIdsLiveData()
+                // just a little hack to continue using `safeObserveWhileNotNull`
+                @Suppress("UNCHECKED_CAST")
+                (allWorkSpecIdsLiveData as LiveData<List<String>?>)
                     .safeObserveWhileNotNull(this, executor) { oldList, newList ->
                         updateWorkIdList(oldList ?: listOf(), newList)
                     }
@@ -123,7 +127,7 @@
      * Observation will last until "null" value is dispatched, then
      * observer will be automatically removed.
      */
-    private fun <T> LiveData<T>.safeObserveWhileNotNull(
+    private fun <T : Any> LiveData<T?>.safeObserveWhileNotNull(
         owner: LifecycleOwner,
         executor: Executor,
         listener: (oldValue: T?, newValue: T) -> Unit
@@ -131,9 +135,9 @@
         mainHandler.post {
             observe(
                 owner,
-                object : Observer<T> {
+                object : Observer<T?> {
                     private var lastValue: T? = null
-                    override fun onChanged(value: T) {
+                    override fun onChanged(value: T?) {
                         if (value == null) {
                             removeObserver(this)
                         } else {
@@ -178,7 +182,7 @@
         workInfoBuilder.isPeriodic = workSpec.isPeriodic
         workInfoBuilder.constraints = workSpec.constraints.toProto()
         workManager.getWorkInfoById(UUID.fromString(id)).let {
-            workInfoBuilder.addAllTags(it.get().tags)
+            workInfoBuilder.addAllTags(it.get()?.tags ?: emptyList())
         }
 
         val workStackBuilder = WorkManagerInspectorProtocol.CallStack.newBuilder()
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index b681410..bc47ebf 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -453,41 +453,50 @@
   }
 
   public abstract class WorkManager {
-    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginUniqueWork(String uniqueWorkName, androidx.work.ExistingWorkPolicy existingWorkPolicy, androidx.work.OneTimeWorkRequest request);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String uniqueWorkName, androidx.work.ExistingWorkPolicy existingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest> requests);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest request);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest> requests);
     method public abstract androidx.work.Operation cancelAllWork();
-    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
-    method public abstract androidx.work.Operation cancelUniqueWork(String);
-    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
-    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
-    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
-    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest!>);
-    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String tag);
+    method public abstract androidx.work.Operation cancelUniqueWork(String uniqueWorkName);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID id);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID id);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest request);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest> requests);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String uniqueWorkName, androidx.work.ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest request);
+    method public androidx.work.Operation enqueueUniqueWork(String uniqueWorkName, androidx.work.ExistingWorkPolicy existingWorkPolicy, androidx.work.OneTimeWorkRequest request);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String uniqueWorkName, androidx.work.ExistingWorkPolicy existingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest> requests);
     method public abstract androidx.work.Configuration getConfiguration();
     method @Deprecated public static androidx.work.WorkManager getInstance();
-    method public static androidx.work.WorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
-    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
-    method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo!> getWorkInfoByIdFlow(java.util.UUID);
-    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
-    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagFlow(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
-    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosFlow(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
-    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkFlow(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
-    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public static androidx.work.WorkManager getInstance(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo?> getWorkInfoById(java.util.UUID id);
+    method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo?> getWorkInfoByIdFlow(java.util.UUID id);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo?> getWorkInfoByIdLiveData(java.util.UUID id);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfos(androidx.work.WorkQuery workQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTag(String tag);
+    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTagFlow(String tag);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTagLiveData(String tag);
+    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo>> getWorkInfosFlow(androidx.work.WorkQuery workQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWork(String uniqueWorkName);
+    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWorkFlow(String uniqueWorkName);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWorkLiveData(String uniqueWorkName);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosLiveData(androidx.work.WorkQuery workQuery);
+    method public static void initialize(android.content.Context context, androidx.work.Configuration configuration);
     method public static boolean isInitialized();
     method public abstract androidx.work.Operation pruneWork();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult> updateWork(androidx.work.WorkRequest request);
+    property public abstract androidx.work.Configuration configuration;
+    field public static final androidx.work.WorkManager.Companion Companion;
+  }
+
+  public static final class WorkManager.Companion {
+    method @Deprecated public androidx.work.WorkManager getInstance();
+    method public androidx.work.WorkManager getInstance(android.content.Context context);
+    method public void initialize(android.content.Context context, androidx.work.Configuration configuration);
+    method public boolean isInitialized();
   }
 
   public enum WorkManager.UpdateResult {
@@ -496,7 +505,7 @@
     enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
   }
 
-  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager!> {
     ctor public WorkManagerInitializer();
     method public androidx.work.WorkManager create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>!>!> dependencies();
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index b681410..bc47ebf 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -453,41 +453,50 @@
   }
 
   public abstract class WorkManager {
-    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginUniqueWork(String uniqueWorkName, androidx.work.ExistingWorkPolicy existingWorkPolicy, androidx.work.OneTimeWorkRequest request);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String uniqueWorkName, androidx.work.ExistingWorkPolicy existingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest> requests);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest request);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest> requests);
     method public abstract androidx.work.Operation cancelAllWork();
-    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
-    method public abstract androidx.work.Operation cancelUniqueWork(String);
-    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
-    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
-    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
-    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest!>);
-    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String tag);
+    method public abstract androidx.work.Operation cancelUniqueWork(String uniqueWorkName);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID id);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID id);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest request);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest> requests);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String uniqueWorkName, androidx.work.ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest request);
+    method public androidx.work.Operation enqueueUniqueWork(String uniqueWorkName, androidx.work.ExistingWorkPolicy existingWorkPolicy, androidx.work.OneTimeWorkRequest request);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String uniqueWorkName, androidx.work.ExistingWorkPolicy existingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest> requests);
     method public abstract androidx.work.Configuration getConfiguration();
     method @Deprecated public static androidx.work.WorkManager getInstance();
-    method public static androidx.work.WorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
-    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
-    method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo!> getWorkInfoByIdFlow(java.util.UUID);
-    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
-    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagFlow(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
-    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosFlow(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
-    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkFlow(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
-    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public static androidx.work.WorkManager getInstance(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo?> getWorkInfoById(java.util.UUID id);
+    method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo?> getWorkInfoByIdFlow(java.util.UUID id);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo?> getWorkInfoByIdLiveData(java.util.UUID id);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfos(androidx.work.WorkQuery workQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTag(String tag);
+    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTagFlow(String tag);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTagLiveData(String tag);
+    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo>> getWorkInfosFlow(androidx.work.WorkQuery workQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWork(String uniqueWorkName);
+    method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWorkFlow(String uniqueWorkName);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWorkLiveData(String uniqueWorkName);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosLiveData(androidx.work.WorkQuery workQuery);
+    method public static void initialize(android.content.Context context, androidx.work.Configuration configuration);
     method public static boolean isInitialized();
     method public abstract androidx.work.Operation pruneWork();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult> updateWork(androidx.work.WorkRequest request);
+    property public abstract androidx.work.Configuration configuration;
+    field public static final androidx.work.WorkManager.Companion Companion;
+  }
+
+  public static final class WorkManager.Companion {
+    method @Deprecated public androidx.work.WorkManager getInstance();
+    method public androidx.work.WorkManager getInstance(android.content.Context context);
+    method public void initialize(android.content.Context context, androidx.work.Configuration configuration);
+    method public boolean isInitialized();
   }
 
   public enum WorkManager.UpdateResult {
@@ -496,7 +505,7 @@
     enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
   }
 
-  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager!> {
     ctor public WorkManagerInitializer();
     method public androidx.work.WorkManager create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>!>!> dependencies();
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt
index 3e3f75c..dfb0087 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt
@@ -32,6 +32,7 @@
 import java.util.concurrent.Executors
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.runBlocking
 import org.junit.Test
@@ -96,12 +97,12 @@
         workManager.enqueue(request)
         val worker = workerFactory.await(request.id) as ProgressUpdatingWorker
 
-        val progress1 = workManager.getWorkInfoByIdFlow(request.id)
+        val progress1 = workManager.getWorkInfoByIdFlow(request.id).filterNotNull()
             .first { it.progress.getInt("progress", 0) != 0 }.progress
         assertThat(progress1.getInt("progress", 0)).isEqualTo(1)
         worker.firstCheckPoint.complete(Unit)
 
-        val progress2 = workManager.getWorkInfoByIdFlow(request.id)
+        val progress2 = workManager.getWorkInfoByIdFlow(request.id).filterNotNull()
             .first { it.progress.getInt("progress", 0) != 1 }.progress
         assertThat(progress2.getInt("progress", 0)).isEqualTo(100)
         worker.secondCheckPoint.complete(Unit)
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/GreedySchedulerTimeoutTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/GreedySchedulerTimeoutTest.kt
index 1f2c690..3b37ebb 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/GreedySchedulerTimeoutTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/GreedySchedulerTimeoutTest.kt
@@ -64,10 +64,10 @@
         val worker = workerFactory.await(request.id)
         val tester = launchTester(workManager.getWorkInfoByIdFlow(request.id))
         val runningWorkInfo = tester.awaitNext()
-        assertThat(runningWorkInfo.state).isEqualTo(WorkInfo.State.RUNNING)
+        assertThat(runningWorkInfo!!.state).isEqualTo(WorkInfo.State.RUNNING)
         runnableScheduler.executedFutureRunnables(TimeUnit.HOURS.toMillis(2))
         val stopInfo = tester.awaitNext()
-        assertThat(stopInfo.state).isEqualTo(WorkInfo.State.ENQUEUED)
+        assertThat(stopInfo!!.state).isEqualTo(WorkInfo.State.ENQUEUED)
         assertThat(worker.stopReason).isEqualTo(STOP_REASON_TIMEOUT)
     }
 
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
index 0bf15bf..be10c1d 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
@@ -84,7 +84,7 @@
         val finishedLatch = CountDownLatch(1)
         env.taskExecutor.mainThreadExecutor.execute {
             wm.getWorkInfoByIdLiveData(dependency.id).observeForever {
-                if (it.state == WorkInfo.State.SUCCEEDED) finishedLatch.countDown()
+                if (it?.state == WorkInfo.State.SUCCEEDED) finishedLatch.countDown()
             }
         }
         assertThat(finishedLatch.await(5, TimeUnit.SECONDS)).isTrue()
@@ -112,7 +112,7 @@
         val finishedLatch = CountDownLatch(1)
         env.taskExecutor.mainThreadExecutor.execute {
             wm.getWorkInfoByIdLiveData(workRequest.id).observeForever {
-                if (it.state == WorkInfo.State.FAILED) finishedLatch.countDown()
+                if (it?.state == WorkInfo.State.FAILED) finishedLatch.countDown()
             }
         }
         assertThat(finishedLatch.await(5, TimeUnit.SECONDS)).isTrue()
@@ -151,7 +151,7 @@
         var running = false
         env.taskExecutor.mainThreadExecutor.execute {
             wm.getWorkInfoByIdLiveData(request.id).observeForever {
-                when (it.state) {
+                when (it?.state) {
                     WorkInfo.State.RUNNING -> {
                         launcher.stopWork(scheduler.tokens.remove(request.stringId).first())
                         running = true
@@ -206,7 +206,7 @@
         val worker = factory.awaitWorker(request.id) as LatchWorker
         env.taskExecutor.mainThreadExecutor.execute {
             wm.getWorkInfoByIdLiveData(request.id).observeForever {
-                when (it.state) {
+                when (it?.state) {
                     WorkInfo.State.RUNNING -> {
                         running = true
                         worker.mLatch.countDown()
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/StopReasonTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/StopReasonTest.kt
index 8bdf6a9..fe10651 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/StopReasonTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/StopReasonTest.kt
@@ -34,6 +34,7 @@
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.runBlocking
 import org.junit.Test
@@ -68,12 +69,12 @@
         workManager.enqueue(request).await()
         val worker = workerFactory.await(request.id)
         val tester = launchTester(workManager.getWorkInfoByIdFlow(request.id))
-        val runningWorkInfo = tester.awaitNext()
+        val runningWorkInfo = tester.awaitNext()!!
         assertThat(runningWorkInfo.state).isEqualTo(WorkInfo.State.RUNNING)
         assertThat(runningWorkInfo.stopReason).isEqualTo(WorkInfo.STOP_REASON_NOT_STOPPED)
 
         fakeChargingTracker.constraintState = false
-        val workInfo = tester.awaitNext()
+        val workInfo = tester.awaitNext()!!
         assertThat(worker.isStopped).isTrue()
         assertThat(worker.stopReason).isEqualTo(STOP_REASON_CONSTRAINT_CHARGING)
         assertThat(workInfo.stopReason).isEqualTo(STOP_REASON_CONSTRAINT_CHARGING)
@@ -84,7 +85,8 @@
         val request = OneTimeWorkRequest.Builder(CompletableWorker::class.java).build()
         workManager.enqueue(request)
         val worker = workerFactory.await(request.id)
-        workManager.getWorkInfoByIdFlow(request.id).first { it.state == WorkInfo.State.RUNNING }
+        workManager.getWorkInfoByIdFlow(request.id).filterNotNull()
+            .first { it.state == WorkInfo.State.RUNNING }
         assertThat(worker.stopReason).isEqualTo(WorkInfo.STOP_REASON_NOT_STOPPED)
     }
 
@@ -93,9 +95,10 @@
         val request = OneTimeWorkRequest.Builder(CompletableWorker::class.java).build()
         workManager.enqueue(request)
         val worker = workerFactory.await(request.id)
-        workManager.getWorkInfoByIdFlow(request.id).first { it.state == WorkInfo.State.RUNNING }
+        workManager.getWorkInfoByIdFlow(request.id).filterNotNull()
+            .first { it.state == WorkInfo.State.RUNNING }
         workManager.cancelWorkById(request.id)
-        val workInfo = workManager.getWorkInfoByIdFlow(request.id)
+        val workInfo = workManager.getWorkInfoByIdFlow(request.id).filterNotNull()
             .first { it.state == WorkInfo.State.CANCELLED }
         assertThat(worker.isStopped).isTrue()
         assertThat(worker.stopReason).isEqualTo(STOP_REASON_CANCELLED_BY_APP)
@@ -108,7 +111,7 @@
             .setInitialDelay(10, TimeUnit.DAYS).build()
         workManager.enqueue(request).await()
         workManager.cancelWorkById(request.id).await()
-        val workInfo = workManager.getWorkInfoById(request.id).await()
+        val workInfo = workManager.getWorkInfoById(request.id).await()!!
         assertThat(workInfo.state).isEqualTo(WorkInfo.State.CANCELLED)
         assertThat(workInfo.stopReason).isEqualTo(WorkInfo.STOP_REASON_NOT_STOPPED)
     }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkInfoFlowsTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkInfoFlowsTest.kt
index d274179..365ffe4 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkInfoFlowsTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkInfoFlowsTest.kt
@@ -70,12 +70,12 @@
         assertThat(tester.awaitNext()).isNull()
         workManager.enqueue(unrelatedRequest)
         workManager.enqueue(request)
-        assertThat(tester.awaitNext().state).isEqualTo(WorkInfo.State.ENQUEUED)
+        assertThat(tester.awaitNext()!!.state).isEqualTo(WorkInfo.State.ENQUEUED)
         fakeChargingTracker.constraintState = true
-        assertThat(tester.awaitNext().state).isEqualTo(WorkInfo.State.RUNNING)
+        assertThat(tester.awaitNext()!!.state).isEqualTo(WorkInfo.State.RUNNING)
         val worker = workerFactory.awaitWorker(request.id) as LatchWorker
         worker.mLatch.countDown()
-        assertThat(tester.awaitNext().state).isEqualTo(WorkInfo.State.SUCCEEDED)
+        assertThat(tester.awaitNext()!!.state).isEqualTo(WorkInfo.State.SUCCEEDED)
     }
 
     @Test
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
index cfc03fa..a453890 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
@@ -51,6 +51,7 @@
 import java.util.concurrent.TimeUnit.DAYS
 import java.util.concurrent.TimeUnit.HOURS
 import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest
 import org.junit.After
@@ -160,7 +161,7 @@
         assertThat(workManager.updateWork(updatedWorkRequest).await())
             .isEqualTo(APPLIED_IMMEDIATELY)
 
-        val info = workManager.getWorkInfoByIdFlow(oneTimeWorkRequest.id).first()
+        val info = workManager.getWorkInfoByIdFlow(oneTimeWorkRequest.id).first()!!
         assertThat(info.tags).contains("test")
         assertThat(info.tags).doesNotContain("previous")
     }
@@ -229,7 +230,7 @@
             .build()
         workManager.enqueue(request).result.await()
         fakeChargingTracker.constraintState = true
-        workManager.getWorkInfoByIdFlow(request.id).first {
+        workManager.getWorkInfoByIdFlow(request.id).filterNotNull().first {
             it.state == State.RUNNING && it.progress.size() != 0
         }
         // will trigger worker to be stopped
@@ -243,7 +244,7 @@
             .addTag("bla")
             .build()
         assertThat(workManager.updateWork(updatedRequest).await()).isEqualTo(APPLIED_IMMEDIATELY)
-        val updatedInfo = workManager.getWorkInfoByIdFlow(request.id).first()
+        val updatedInfo = workManager.getWorkInfoByIdFlow(request.id).first()!!
         assertThat(updatedInfo.tags).contains("bla")
         assertThat(updatedInfo.progress).isEqualTo(Data.EMPTY)
     }
@@ -259,7 +260,7 @@
         val updatedStep2 = OneTimeWorkRequest.Builder(TestWorker::class.java)
             .setId(step2.id).addTag("updated").build()
         assertThat(workManager.updateWork(updatedStep2).await()).isEqualTo(APPLIED_IMMEDIATELY)
-        val workInfo = workManager.getWorkInfoById(step2.id).await()
+        val workInfo = workManager.getWorkInfoById(step2.id).await()!!
         assertThat(workInfo.state).isEqualTo(State.BLOCKED)
         assertThat(workInfo.tags).contains("updated")
     }
@@ -272,7 +273,7 @@
             .setConstraints(Constraints(requiresCharging = true)).build()
         val step2 = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         workManager.beginWith(step1).then(step2).enqueue().result.await()
-        val workInfo = workManager.getWorkInfoById(step2.id).await()
+        val workInfo = workManager.getWorkInfoById(step2.id).await()!!
         assertThat(workInfo.state).isEqualTo(State.BLOCKED)
         val updatedStep1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
             .setId(step1.id).build()
@@ -292,7 +293,7 @@
         val updatedStep2 = OneTimeWorkRequest.Builder(TestWorker::class.java)
             .setId(step2.id).addTag("updated").build()
         assertThat(workManager.updateWork(updatedStep2).await()).isEqualTo(APPLIED_IMMEDIATELY)
-        val workInfo = workManager.getWorkInfoById(step2.id).await()
+        val workInfo = workManager.getWorkInfoById(step2.id).await()!!
         assertThat(workInfo.state).isEqualTo(State.BLOCKED)
         assertThat(workInfo.tags).contains("updated")
     }
@@ -306,7 +307,7 @@
         val step2 = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step1)
         workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step2)
-        val workInfo = workManager.getWorkInfoById(step2.id).await()
+        val workInfo = workManager.getWorkInfoById(step2.id).await()!!
         assertThat(workInfo.state).isEqualTo(State.BLOCKED)
         val updatedStep1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
             .setId(step1.id).build()
@@ -365,7 +366,7 @@
         assertThat(worker.tags).doesNotContain("updated")
         worker.result.complete(Result.success())
         workManager.awaitWorkerEnqueued(request.id)
-        val newTags = workManager.getWorkInfoById(request.id).await().tags
+        val newTags = workManager.getWorkInfoById(request.id).await()!!.tags
         assertThat(newTags).contains("updated")
         assertThat(newTags).doesNotContain("original")
     }
@@ -387,7 +388,7 @@
 
         assertThat(workManager.updateWork(updatedRequest).await()).isEqualTo(APPLIED_IMMEDIATELY)
 
-        val newTags = workManager.getWorkInfoById(request.id).await().tags
+        val newTags = workManager.getWorkInfoById(request.id).await()!!.tags
         assertThat(newTags).contains("updated")
         assertThat(newTags).doesNotContain("original")
         val workSpec = env.db.workSpecDao().getWorkSpec(request.stringId)!!
@@ -480,7 +481,7 @@
             .isEqualTo(APPLIED_IMMEDIATELY)
         val worker = workerFactory.await(oneTimeWorkRequest.id) as WorkerWithParam
         assertThat(worker.generation).isEqualTo(1)
-        val workInfo = workManager.getWorkInfoById(oneTimeWorkRequest.id).await()
+        val workInfo = workManager.getWorkInfoById(oneTimeWorkRequest.id).await()!!
         assertThat(workInfo.generation).isEqualTo(1)
     }
 
@@ -503,7 +504,7 @@
                 .build()
         ).await()
 
-        val workInfo = workManager.getWorkInfoById(request.id).await()
+        val workInfo = workManager.getWorkInfoById(request.id).await()!!
         assertThat(workInfo.nextScheduleTimeMillis).isEqualTo(nextRunTimeMillis)
     }
 
@@ -526,7 +527,7 @@
                 .setNextScheduleTimeOverride(overrideScheduleTimeMillis)
                 .build()
         ).await()
-        val workInfo = workManager.getWorkInfoById(request.id).await()
+        val workInfo = workManager.getWorkInfoById(request.id).await()!!
         assertThat(workInfo.nextScheduleTimeMillis).isEqualTo(overrideScheduleTimeMillis)
 
         workManager.updateWork(
@@ -537,7 +538,7 @@
                 .build()
         ).await()
 
-        val workInfo2 = workManager.getWorkInfoById(request.id).await()
+        val workInfo2 = workManager.getWorkInfoById(request.id).await()!!
         assertThat(workInfo2.nextScheduleTimeMillis).isEqualTo(overrideScheduleTimeMillis2)
     }
 
@@ -563,7 +564,7 @@
                 .build()
         ).await()
 
-        val workInfo = workManager.getWorkInfoById(request.id).await()
+        val workInfo = workManager.getWorkInfoById(request.id).await()!!
         assertThat(workInfo.nextScheduleTimeMillis).isEqualTo(overrideScheduleTimeMillis)
         val workSpec = env.db.workSpecDao().getWorkSpec(request.stringId)!!
         // attemptCount is still kept, just not used in the schedule time calculation.
@@ -592,7 +593,7 @@
                 .build()
         ).await()
 
-        val workInfo = workManager.getWorkInfoById(request.id).await()
+        val workInfo = workManager.getWorkInfoById(request.id).await()!!
         assertThat(workInfo.nextScheduleTimeMillis).isEqualTo(
             testClock.currentTimeMillis + DAYS.toMillis(2)
         )
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTestKt.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTestKt.kt
index b5da863..0dec209 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTestKt.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTestKt.kt
@@ -34,6 +34,7 @@
 import androidx.work.worker.CompletableWorker
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executors
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.runBlocking
 import org.junit.Test
@@ -71,7 +72,7 @@
             .complete(Success(workDataOf(intTag to 1, stringTag to "hello")))
         (workerFactory.await(secondWork.id) as CompletableWorker).result
             .complete(Success(workDataOf(intTag to 3)))
-        val info = workManager.getWorkInfoByIdFlow(thirdId)
+        val info = workManager.getWorkInfoByIdFlow(thirdId).filterNotNull()
             .first { it.state == WorkInfo.State.SUCCEEDED }
         assertThat(info.outputData.size()).isEqualTo(2)
         assertThat(info.outputData.getStringArray(stringTag)).isEqualTo(arrayOf("hello"))
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/WorkManagerImplTestKt.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/WorkManagerImplTestKt.kt
index 702d26d..b7ea9b0 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/WorkManagerImplTestKt.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/WorkManagerImplTestKt.kt
@@ -51,7 +51,7 @@
         preferenceUtils.lastCancelAllTimeMillis = 0L
 
         val testLifecycleOwner = TestLifecycleOwner()
-        val cancelAllTimeLiveData = workManager.lastCancelAllTimeMillisLiveData
+        val cancelAllTimeLiveData = workManager.getLastCancelAllTimeMillisLiveData()
         val firstValueLatch = CountDownLatch(1)
         val secondValueLatch = CountDownLatch(1)
         var firstCancelAll = -1L
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.kt
index 7fb6887..ebc7a95 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.kt
@@ -50,6 +50,7 @@
 import java.util.UUID
 import java.util.concurrent.Executors
 import kotlin.reflect.KClass
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.launch
@@ -117,7 +118,7 @@
         workerFactory.await(workerWrapper.workSpecId)
         future.await()
         assertThat(workManager.awaitNotRunning(workerWrapper)).isEqualTo(State.SUCCEEDED)
-        val outputData = workManager.getWorkInfoById(workerWrapper.workSpecId).await().outputData
+        val outputData = workManager.getWorkInfoById(workerWrapper.workSpecId).await()!!.outputData
         assertThat(outputData.getBoolean(TEST_ARGUMENT_NAME, false)).isTrue()
     }
 
@@ -204,7 +205,8 @@
 }
 
 private suspend fun WorkManager.awaitNotRunning(workerWrapper: WorkerWrapper) =
-    getWorkInfoByIdFlow(workerWrapper.workSpecId).first { it.state != State.RUNNING }.state
+    getWorkInfoByIdFlow(workerWrapper.workSpecId).filterNotNull()
+        .first { it.state != State.RUNNING }.state
 
 private val WorkerWrapper.workSpecId
     get() = UUID.fromString(workGenerationalId.workSpecId)
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/testutils/WorkManagerExt.kt b/work/work-runtime/src/androidTest/java/androidx/work/testutils/WorkManagerExt.kt
index 3558523..ac464c4 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/testutils/WorkManagerExt.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/testutils/WorkManagerExt.kt
@@ -19,10 +19,11 @@
 import androidx.work.WorkInfo
 import androidx.work.WorkManager
 import java.util.UUID
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 
 suspend fun WorkManager.awaitWorkerFinished(id: UUID): WorkInfo =
-    getWorkInfoByIdFlow(id).first { it.state.isFinished }
+    getWorkInfoByIdFlow(id).filterNotNull().first { it.state.isFinished }
 
 suspend fun WorkManager.awaitWorkerEnqueued(id: UUID): WorkInfo =
-    getWorkInfoByIdFlow(id).first { it.state == WorkInfo.State.ENQUEUED }
+    getWorkInfoByIdFlow(id).filterNotNull().first { it.state == WorkInfo.State.ENQUEUED }
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkManager.java b/work/work-runtime/src/main/java/androidx/work/WorkManager.java
deleted file mode 100644
index bd3213b..0000000
--- a/work/work-runtime/src/main/java/androidx/work/WorkManager.java
+++ /dev/null
@@ -1,706 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work;
-
-import android.annotation.SuppressLint;
-import android.app.PendingIntent;
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.lifecycle.LiveData;
-import androidx.work.impl.WorkManagerImpl;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-
-import kotlinx.coroutines.flow.Flow;
-
-/**
- * WorkManager is the recommended library for persistent work.
- * Scheduled work is guaranteed to execute sometime after its {@link Constraints} are met.
- * WorkManager allows observation of work status and the ability to create complex chains of work.
- * <p>
- * WorkManager uses an underlying job dispatching service when available based on the following
- * criteria:
- * <p><ul>
- * <li>Uses JobScheduler for API 23+
- * <li>Uses a custom AlarmManager + BroadcastReceiver implementation for API 14-22</ul>
- * <p>
- * All work must be done in a {@link ListenableWorker} class.  A simple implementation,
- * {@link Worker}, is recommended as the starting point for most developers.  With the optional
- * dependencies, you can also use {@code CoroutineWorker} or {@code RxWorker}.  All background work
- * is given a maximum of ten minutes to finish its execution.  After this time has expired, the
- * worker will be signalled to stop.
- * <p>
- * There are two types of work supported by WorkManager: {@link OneTimeWorkRequest} and
- * {@link PeriodicWorkRequest}.  You can enqueue requests using WorkManager as follows:
- *
- * <pre class="prettyprint">
- * WorkManager workManager = WorkManager.getInstance(Context);
- * workManager.enqueue(new OneTimeWorkRequest.Builder(FooWorker.class).build());
- * </pre>
- *
- * A {@link WorkRequest} has an associated id that can be used for lookups and observation as
- * follows:
- *
- * <pre class="prettyprint">
- * WorkRequest request = new OneTimeWorkRequest.Builder(FooWorker.class).build();
- * workManager.enqueue(request);
- * LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(request.getId());
- * status.observe(...);
- * </pre>
- *
- * You can also use the id for cancellation:
- *
- * <pre class="prettyprint">
- * WorkRequest request = new OneTimeWorkRequest.Builder(FooWorker.class).build();
- * workManager.enqueue(request);
- * workManager.cancelWorkById(request.getId());
- * </pre>
- *
- * You can chain work as follows:
- *
- * <pre class="prettyprint">
- * WorkRequest request1 = new OneTimeWorkRequest.Builder(FooWorker.class).build();
- * WorkRequest request2 = new OneTimeWorkRequest.Builder(BarWorker.class).build();
- * WorkRequest request3 = new OneTimeWorkRequest.Builder(BazWorker.class).build();
- * workManager.beginWith(request1, request2).then(request3).enqueue();
- * </pre>
- *
- * Each call to {@link #beginWith(OneTimeWorkRequest)} or {@link #beginWith(List)} returns a
- * {@link WorkContinuation} upon which you can call
- * {@link WorkContinuation#then(OneTimeWorkRequest)} or {@link WorkContinuation#then(List)} to
- * chain further work.  This allows for creation of complex chains of work.  For example, to create
- * a chain like this:
- *
- * <pre>
- *            A
- *            |
- *      +----------+
- *      |          |
- *      B          C
- *      |
- *   +----+
- *   |    |
- *   D    E             </pre>
- *
- * you would enqueue them as follows:
- *
- * <pre class="prettyprint">
- * WorkContinuation continuation = workManager.beginWith(A);
- * continuation.then(B).then(D, E).enqueue();  // A is implicitly enqueued here
- * continuation.then(C).enqueue();
- * </pre>
- *
- * Work is eligible for execution when all of its prerequisites are complete.  If any of its
- * prerequisites fail or are cancelled, the work will never run.
- * <p>
- * WorkRequests can accept {@link Constraints}, inputs (see {@link Data}), and backoff criteria.
- * WorkRequests can be tagged with human-readable Strings
- * (see {@link WorkRequest.Builder#addTag(String)}), and chains of work can be given a
- * uniquely-identifiable name (see
- * {@link #beginUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)}).
- * <p>
- * <h3 id="initializing">Initializing WorkManager</h3>
- * <p>
- * By default, WorkManager auto-initializes itself using a built-in {@code ContentProvider}.
- * ContentProviders are created and run before the {@code Application} object, so this allows the
- * WorkManager singleton to be setup before your code can run in most cases.  This is suitable for
- * most developers.  However, you can provide a custom {@link Configuration} by using
- * {@link Configuration.Provider} or
- * {@link WorkManager#initialize(android.content.Context, androidx.work.Configuration)}.
- * <p>
- * <h3 id="worker_class_names">Renaming and Removing ListenableWorker Classes</h3>
- * <p>
- * Exercise caution in renaming classes derived from {@link ListenableWorker}s.  WorkManager stores
- * the class name in its internal database when the {@link WorkRequest} is enqueued so it can later
- * create an instance of that worker when constraints are met.  Unless otherwise specified in the
- * WorkManager {@link Configuration}, this is done in the default {@link WorkerFactory} which tries
- * to reflectively create the ListenableWorker object.  Therefore, renaming or removing these
- * classes is dangerous - if there is pending work with the given class, it will fail permanently
- * if the class cannot be found.  If you are using a custom WorkerFactory, make sure you properly
- * handle cases where the class is not found so that your code does not crash.
- * <p>
- * In case it is desirable to rename a class, implement a custom WorkerFactory that instantiates the
- * right ListenableWorker for the old class name.
- * */
-// Suppressing Metalava checks for added abstract methods in WorkManager.
-// WorkManager cannot be extended, because the constructor is marked @Restricted
-@SuppressLint("AddedAbstractMethod")
-public abstract class WorkManager {
-
-    /**
-     * Retrieves the {@code default} singleton instance of {@link WorkManager}.
-     *
-     * @return The singleton instance of {@link WorkManager}; this may be {@code null} in unusual
-     *         circumstances where you have disabled automatic initialization and have failed to
-     *         manually call {@link #initialize(Context, Configuration)}.
-     * @throws IllegalStateException If WorkManager is not initialized properly as per the exception
-     *                               message.
-     * @deprecated Call {@link WorkManager#getInstance(Context)} instead.
-     */
-    @Deprecated
-    public static @NonNull WorkManager getInstance() {
-        WorkManager workManager = WorkManagerImpl.getInstance();
-        if (workManager == null) {
-            throw new IllegalStateException("WorkManager is not initialized properly.  The most "
-                    + "likely cause is that you disabled WorkManagerInitializer in your manifest "
-                    + "but forgot to call WorkManager#initialize in your Application#onCreate or a "
-                    + "ContentProvider.");
-        } else {
-            return workManager;
-        }
-    }
-
-    /**
-     * Retrieves the {@code default} singleton instance of {@link WorkManager}.
-     *
-     * @param context A {@link Context} for on-demand initialization.
-     * @return The singleton instance of {@link WorkManager}; this may be {@code null} in unusual
-     *         circumstances where you have disabled automatic initialization and have failed to
-     *         manually call {@link #initialize(Context, Configuration)}.
-     * @throws IllegalStateException If WorkManager is not initialized properly
-     */
-    public static @NonNull WorkManager getInstance(@NonNull Context context) {
-        return WorkManagerImpl.getInstance(context);
-    }
-
-    /**
-     * Used to do a one-time initialization of the {@link WorkManager} singleton with a custom
-     * {@link Configuration}.  By default, this method should not be called because WorkManager is
-     * automatically initialized.  To initialize WorkManager yourself, please follow these steps:
-     * <p><ul>
-     * <li>Disable {@code androidx.work.WorkManagerInitializer} in your manifest.
-     * <li>Invoke this method in {@code Application#onCreate} or a {@code ContentProvider}. Note
-     * that this method <b>must</b> be invoked in one of these two places or you risk getting a
-     * {@code NullPointerException} in {@link #getInstance(Context)}.
-     * </ul></p>
-     * <p>
-     * This method throws an {@link IllegalStateException} when attempting to initialize in
-     * direct boot mode.
-     * <p>
-     * This method throws an exception if it is called multiple times.
-     *
-     * @param context A {@link Context} object for configuration purposes. Internally, this class
-     *                will call {@link Context#getApplicationContext()}, so you may safely pass in
-     *                any Context without risking a memory leak.
-     * @param configuration The {@link Configuration} for used to set up WorkManager.
-     * @see Configuration.Provider for on-demand initialization.
-     */
-    public static void initialize(@NonNull Context context, @NonNull Configuration configuration) {
-        WorkManagerImpl.initialize(context, configuration);
-    }
-
-    /**
-     * Provides a way to check if {@link WorkManager} is initialized in this process.
-     *
-     * @return {@code true} if {@link WorkManager} has been initialized in this process.
-     */
-    public static boolean isInitialized() {
-        return WorkManagerImpl.isInitialized();
-    }
-
-    /**
-     * Provides the {@link Configuration} instance that {@link WorkManager} was initialized with.
-     *
-     * @return The {@link Configuration} instance that {@link WorkManager} was initialized with.
-     */
-    @NonNull
-    public abstract Configuration getConfiguration();
-
-    /**
-     * Enqueues one item for background processing.
-     *
-     * @param workRequest The {@link WorkRequest} to enqueue
-     * @return An {@link Operation} that can be used to determine when the enqueue has completed
-     */
-    @NonNull
-    public final Operation enqueue(@NonNull WorkRequest workRequest) {
-        return enqueue(Collections.singletonList(workRequest));
-    }
-
-    /**
-     * Enqueues one or more items for background processing.
-     *
-     * @param requests One or more {@link WorkRequest} to enqueue
-     * @return An {@link Operation} that can be used to determine when the enqueue has completed
-     */
-    @NonNull
-    public abstract Operation enqueue(@NonNull List<? extends WorkRequest> requests);
-
-    /**
-     * Begins a chain with one or more {@link OneTimeWorkRequest}s, which can be enqueued together
-     * in the future using {@link WorkContinuation#enqueue()}.
-     * <p>
-     * If any work in the chain fails or is cancelled, all of its dependent work inherits that state
-     * and will never run.
-     *
-     * @param work One or more {@link OneTimeWorkRequest} to start a chain of work
-     * @return A {@link WorkContinuation} that allows for further chaining of dependent
-     *         {@link OneTimeWorkRequest}
-     */
-    public final @NonNull WorkContinuation beginWith(@NonNull OneTimeWorkRequest work) {
-        return beginWith(Collections.singletonList(work));
-    }
-
-    /**
-     * Begins a chain with one or more {@link OneTimeWorkRequest}s, which can be enqueued together
-     * in the future using {@link WorkContinuation#enqueue()}.
-     * <p>
-     * If any work in the chain fails or is cancelled, all of its dependent work inherits that state
-     * and will never run.
-     *
-     * @param work One or more {@link OneTimeWorkRequest} to start a chain of work
-     * @return A {@link WorkContinuation} that allows for further chaining of dependent
-     *         {@link OneTimeWorkRequest}
-     */
-    public abstract @NonNull WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work);
-
-    /**
-     * This method allows you to begin unique chains of work for situations where you only want one
-     * chain with a given name to be active at a time.  For example, you may only want one sync
-     * operation to be active.  If there is one pending, you can choose to let it run or replace it
-     * with your new work.
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this set of work.
-     * <p>
-     * If this method determines that new work should be enqueued and run, all records of previous
-     * work with {@code uniqueWorkName} will be pruned.  If this method determines that new work
-     * should NOT be run, then the entire chain will be considered a no-op.
-     * <p>
-     * If any work in the chain fails or is cancelled, all of its dependent work inherits that state
-     * and will never run.  This is particularly important if you are using {@code APPEND} as your
-     * {@link ExistingWorkPolicy}.
-     *
-     * @param uniqueWorkName A unique name which for this chain of work
-     * @param existingWorkPolicy An {@link ExistingWorkPolicy}
-     * @param work The {@link OneTimeWorkRequest} to enqueue. {@code REPLACE} ensures that if there
-     *             is pending work labelled with {@code uniqueWorkName}, it will be cancelled and
-     *             the new work will run. {@code KEEP} will run the new sequence of work only if
-     *             there is no pending work labelled with {@code uniqueWorkName}.  {@code APPEND}
-     *             will create a new sequence of work if there is no existing work with
-     *             {@code uniqueWorkName}; otherwise, {@code work} will be added as a child of all
-     *             leaf nodes labelled with {@code uniqueWorkName}.
-     * @return A {@link WorkContinuation} that allows further chaining
-     */
-    public final @NonNull WorkContinuation beginUniqueWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull OneTimeWorkRequest work) {
-        return beginUniqueWork(uniqueWorkName, existingWorkPolicy, Collections.singletonList(work));
-    }
-
-    /**
-     * This method allows you to begin unique chains of work for situations where you only want one
-     * chain with a given name to be active at a time.  For example, you may only want one sync
-     * operation to be active.  If there is one pending, you can choose to let it run or replace it
-     * with your new work.
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this set of work.
-     * <p>
-     * If this method determines that new work should be enqueued and run, all records of previous
-     * work with {@code uniqueWorkName} will be pruned.  If this method determines that new work
-     * should NOT be run, then the entire chain will be considered a no-op.
-     * <p>
-     * If any work in the chain fails or is cancelled, all of its dependent work inherits that state
-     * and will never run.  This is particularly important if you are using {@code APPEND} as your
-     * {@link ExistingWorkPolicy}.
-     *
-     * @param uniqueWorkName A unique name which for this chain of work
-     * @param existingWorkPolicy An {@link ExistingWorkPolicy}; see below for more information
-     * @param work One or more {@link OneTimeWorkRequest} to enqueue. {@code REPLACE} ensures that
-     *             if there is pending work labelled with {@code uniqueWorkName}, it will be
-     *             cancelled and the new work will run. {@code KEEP} will run the new sequence of
-     *             work only if there is no pending work labelled with {@code uniqueWorkName}.
-     *             {@code APPEND} will create a new sequence of work if there is no
-     *             existing work with {@code uniqueWorkName}; otherwise, {@code work} will be added
-     *             as a child of all leaf nodes labelled with {@code uniqueWorkName}.
-     * @return A {@link WorkContinuation} that allows further chaining
-     */
-    public abstract @NonNull WorkContinuation beginUniqueWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull List<OneTimeWorkRequest> work);
-
-
-    /**
-     * This method allows you to enqueue {@code work} requests to a uniquely-named
-     * {@link WorkContinuation}, where only one continuation of a particular name can be active at
-     * a time. For example, you may only want one sync operation to be active. If there is one
-     * pending, you can choose to let it run or replace it with your new work.
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this {@link WorkContinuation}.
-     *
-     * @param uniqueWorkName A unique name which for this operation
-     * @param existingWorkPolicy An {@link ExistingWorkPolicy}; see below for more information
-     * @param work The {@link OneTimeWorkRequest}s to enqueue. {@code REPLACE} ensures that if there
-     *             is pending work labelled with {@code uniqueWorkName}, it will be cancelled and
-     *             the new work will run. {@code KEEP} will run the new OneTimeWorkRequests only if
-     *             there is no pending work labelled with {@code uniqueWorkName}.  {@code APPEND}
-     *             will append the OneTimeWorkRequests as leaf nodes labelled with
-     *             {@code uniqueWorkName}.
-     * @return An {@link Operation} that can be used to determine when the enqueue has completed
-     */
-    @NonNull
-    public Operation enqueueUniqueWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull OneTimeWorkRequest work) {
-        return enqueueUniqueWork(
-                uniqueWorkName,
-                existingWorkPolicy,
-                Collections.singletonList(work));
-    }
-
-    /**
-     * This method allows you to enqueue {@code work} requests to a uniquely-named
-     * {@link WorkContinuation}, where only one continuation of a particular name can be active at
-     * a time. For example, you may only want one sync operation to be active. If there is one
-     * pending, you can choose to let it run or replace it with your new work.
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this {@link WorkContinuation}.
-     *
-     * @param uniqueWorkName A unique name which for this operation
-     * @param existingWorkPolicy An {@link ExistingWorkPolicy}
-     * @param work {@link OneTimeWorkRequest}s to enqueue. {@code REPLACE} ensures
-     *                     that if there is pending work labelled with {@code uniqueWorkName}, it
-     *                     will be cancelled and the new work will run. {@code KEEP} will run the
-     *                     new OneTimeWorkRequests only if there is no pending work labelled with
-     *                     {@code uniqueWorkName}. {@code APPEND} will append the
-     *                     OneTimeWorkRequests as leaf nodes labelled with {@code uniqueWorkName}.
-     * @return An {@link Operation} that can be used to determine when the enqueue has completed
-     */
-    @NonNull
-    public abstract Operation enqueueUniqueWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull List<OneTimeWorkRequest> work);
-
-    /**
-     * This method allows you to enqueue a uniquely-named {@link PeriodicWorkRequest}, where only
-     * one PeriodicWorkRequest of a particular name can be active at a time.  For example, you may
-     * only want one sync operation to be active.  If there is one pending, you can choose to let it
-     * run or replace it with your new work.
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this PeriodicWorkRequest.
-     *
-     * @param uniqueWorkName A unique name which for this operation
-     * @param existingPeriodicWorkPolicy An {@link ExistingPeriodicWorkPolicy}
-     * @param periodicWork A {@link PeriodicWorkRequest} to enqueue. {@code REPLACE} ensures that if
-     *                     there is pending work labelled with {@code uniqueWorkName}, it will be
-     *                     cancelled and the new work will run. {@code KEEP} will run the new
-     *                     PeriodicWorkRequest only if there is no pending work labelled with
-     *                     {@code uniqueWorkName}.
-     * @return An {@link Operation} that can be used to determine when the enqueue has completed
-     */
-    @NonNull
-    public abstract Operation enqueueUniquePeriodicWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
-            @NonNull PeriodicWorkRequest periodicWork);
-
-    /**
-     * Cancels work with the given id if it isn't finished.  Note that cancellation is a best-effort
-     * policy and work that is already executing may continue to run.  Upon cancellation,
-     * {@link ListenableWorker#onStopped()} will be invoked for any affected workers.
-     *
-     * @param id The id of the work
-     * @return An {@link Operation} that can be used to determine when the cancelWorkById has
-     * completed
-     */
-    public abstract @NonNull Operation cancelWorkById(@NonNull UUID id);
-
-    /**
-     * Cancels all unfinished work with the given tag.  Note that cancellation is a best-effort
-     * policy and work that is already executing may continue to run.  Upon cancellation,
-     * {@link ListenableWorker#onStopped()} will be invoked for any affected workers.
-     *
-     * @param tag The tag used to identify the work
-     * @return An {@link Operation} that can be used to determine when the cancelAllWorkByTag has
-     * completed
-     */
-    public abstract @NonNull Operation cancelAllWorkByTag(@NonNull String tag);
-
-    /**
-     * Cancels all unfinished work in the work chain with the given name.  Note that cancellation is
-     * a best-effort policy and work that is already executing may continue to run.  Upon
-     * cancellation, {@link ListenableWorker#onStopped()} will be invoked for any affected workers.
-     *
-     * @param uniqueWorkName The unique name used to identify the chain of work
-     * @return An {@link Operation} that can be used to determine when the cancelUniqueWork has
-     * completed
-     */
-    public abstract @NonNull Operation cancelUniqueWork(@NonNull String uniqueWorkName);
-
-    /**
-     * Cancels all unfinished work.  <b>Use this method with extreme caution!</b>  By invoking it,
-     * you will potentially affect other modules or libraries in your codebase.  It is strongly
-     * recommended that you use one of the other cancellation methods at your disposal.
-     * <p>
-     * Upon cancellation, {@link ListenableWorker#onStopped()} will be invoked for any affected
-     * workers.
-     *
-     * @return An {@link Operation} that can be used to determine when the cancelAllWork has
-     * completed
-     */
-    public abstract @NonNull Operation cancelAllWork();
-
-    /**
-     * Creates a {@link PendingIntent} which can be used to cancel a {@link WorkRequest} with the
-     * given {@code id}.
-     *
-     * @param id      The {@link WorkRequest} id.
-     * @return The {@link PendingIntent} that can be used to cancel the {@link WorkRequest}.
-     */
-    public abstract @NonNull PendingIntent createCancelPendingIntent(@NonNull UUID id);
-
-    /**
-     * Prunes all eligible finished work from the internal database.  Eligible work must be finished
-     * ({@link WorkInfo.State#SUCCEEDED}, {@link WorkInfo.State#FAILED}, or
-     * {@link WorkInfo.State#CANCELLED}), with zero unfinished dependents.
-     * <p>
-     * <b>Use this method with caution</b>; by invoking it, you (and any modules and libraries in
-     * your codebase) will no longer be able to observe the {@link WorkInfo} of the pruned work.
-     * You do not normally need to call this method - WorkManager takes care to auto-prune its work
-     * after a sane period of time.  This method also ignores the
-     * {@link OneTimeWorkRequest.Builder#keepResultsForAtLeast(long, TimeUnit)} policy.
-     *
-     * @return An {@link Operation} that can be used to determine when the pruneWork has
-     * completed
-     */
-    public abstract @NonNull Operation pruneWork();
-
-    /**
-     * Gets a {@link LiveData} of the last time all work was cancelled.  This method is intended for
-     * use by library and module developers who have dependent data in their own repository that
-     * must be updated or deleted in case someone cancels their work without their prior knowledge.
-     *
-     * @return A {@link LiveData} of the timestamp ({@code System#getCurrentTimeMillis()}) when
-     *         {@link #cancelAllWork()} was last invoked; this timestamp may be {@code 0L} if this
-     *         never occurred
-     */
-    public abstract @NonNull LiveData<Long> getLastCancelAllTimeMillisLiveData();
-
-    /**
-     * Gets a {@link ListenableFuture} of the last time all work was cancelled.  This method is
-     * intended for use by library and module developers who have dependent data in their own
-     * repository that must be updated or deleted in case someone cancels their work without
-     * their prior knowledge.
-     *
-     * @return A {@link ListenableFuture} of the timestamp ({@code System#getCurrentTimeMillis()})
-     *         when {@link #cancelAllWork()} was last invoked; this timestamp may be {@code 0L} if
-     *         this never occurred
-     */
-    public abstract @NonNull ListenableFuture<Long> getLastCancelAllTimeMillis();
-
-    /**
-     * Gets a {@link LiveData} of the {@link WorkInfo} for a given work id.
-     *
-     * @param id The id of the work
-     * @return A {@link LiveData} of the {@link WorkInfo} associated with {@code id}; note that
-     *         this {@link WorkInfo} may be {@code null} if {@code id} is not known to
-     *         WorkManager.
-     */
-    public abstract @NonNull LiveData<WorkInfo> getWorkInfoByIdLiveData(@NonNull UUID id);
-
-    /**
-     * Gets a {@link Flow} of the {@link WorkInfo} for a given work id.
-     *
-     * @param id The id of the work
-     * @return A {@link Flow} of the {@link WorkInfo} associated with {@code id}; note that
-     *         this {@link WorkInfo} may be {@code null} if {@code id} is not known to
-     *         WorkManager.
-     */
-    public abstract @NonNull Flow<WorkInfo> getWorkInfoByIdFlow(@NonNull UUID id);
-
-    /**
-     * Gets a {@link ListenableFuture} of the {@link WorkInfo} for a given work id.
-     *
-     * @param id The id of the work
-     * @return A {@link ListenableFuture} of the {@link WorkInfo} associated with {@code id};
-     * note that this {@link WorkInfo} may be {@code null} if {@code id} is not known to
-     * WorkManager
-     */
-    public abstract @NonNull ListenableFuture<WorkInfo> getWorkInfoById(@NonNull UUID id);
-
-    /**
-     * Gets a {@link LiveData} of the {@link WorkInfo} for all work for a given tag.
-     *
-     * @param tag The tag of the work
-     * @return A {@link LiveData} list of {@link WorkInfo} for work tagged with {@code tag}
-     */
-    public abstract @NonNull LiveData<List<WorkInfo>> getWorkInfosByTagLiveData(
-            @NonNull String tag);
-
-    /**
-     * Gets a {@link Flow} of the {@link WorkInfo} for all work for a given tag.
-     *
-     * @param tag The tag of the work
-     * @return A {@link Flow} list of {@link WorkInfo} for work tagged with {@code tag}
-     */
-    public abstract @NonNull Flow<List<WorkInfo>> getWorkInfosByTagFlow(@NonNull String tag);
-
-    /**
-     * Gets a {@link ListenableFuture} of the {@link WorkInfo} for all work for a given tag.
-     *
-     * @param tag The tag of the work
-     * @return A {@link ListenableFuture} list of {@link WorkInfo} for work tagged with
-     * {@code tag}
-     */
-    public abstract @NonNull ListenableFuture<List<WorkInfo>> getWorkInfosByTag(
-            @NonNull String tag);
-
-    /**
-     * Gets a {@link LiveData} of the {@link WorkInfo} for all work in a work chain with a given
-     * unique name.
-     *
-     * @param uniqueWorkName The unique name used to identify the chain of work
-     * @return A {@link LiveData} of the {@link WorkInfo} for work in the chain named
-     *         {@code uniqueWorkName}
-     */
-    public abstract @NonNull LiveData<List<WorkInfo>> getWorkInfosForUniqueWorkLiveData(
-            @NonNull String uniqueWorkName);
-
-    /**
-     * Gets a {@link Flow} of the {@link WorkInfo} for all work in a work chain with a given
-     * unique name.
-     *
-     * @param uniqueWorkName The unique name used to identify the chain of work
-     * @return A {@link Flow} of the {@link WorkInfo} for work in the chain named
-     *         {@code uniqueWorkName}
-     */
-    public abstract @NonNull Flow<List<WorkInfo>> getWorkInfosForUniqueWorkFlow(
-            @NonNull String uniqueWorkName);
-
-    /**
-     * Gets a {@link ListenableFuture} of the {@link WorkInfo} for all work in a work chain
-     * with a given unique name.
-     *
-     * @param uniqueWorkName The unique name used to identify the chain of work
-     * @return A {@link ListenableFuture} of the {@link WorkInfo} for work in the chain named
-     *         {@code uniqueWorkName}
-     */
-    public abstract @NonNull ListenableFuture<List<WorkInfo>> getWorkInfosForUniqueWork(
-            @NonNull String uniqueWorkName);
-
-    /**
-     * Gets the {@link LiveData} of the {@link List} of {@link WorkInfo} for all work
-     * referenced by the {@link WorkQuery} specification.
-     *
-     * @param workQuery The work query specification
-     * @return A {@link LiveData} of the {@link List} of {@link WorkInfo} for work
-     * referenced by this {@link WorkQuery}.
-     */
-    public abstract @NonNull LiveData<List<WorkInfo>> getWorkInfosLiveData(
-            @NonNull WorkQuery workQuery);
-
-    /**
-     * Gets the {@link Flow} of the {@link List} of {@link WorkInfo} for all work
-     * referenced by the {@link WorkQuery} specification.
-     *
-     * @param workQuery The work query specification
-     * @return A {@link Flow} of the {@link List} of {@link WorkInfo} for work
-     * referenced by this {@link WorkQuery}.
-     */
-    public abstract @NonNull Flow<List<WorkInfo>> getWorkInfosFlow(
-            @NonNull WorkQuery workQuery);
-
-    /**
-     * Gets the {@link ListenableFuture} of the {@link List} of {@link WorkInfo} for all work
-     * referenced by the {@link WorkQuery} specification.
-     *
-     * @param workQuery The work query specification
-     * @return A {@link ListenableFuture} of the {@link List} of {@link WorkInfo} for work
-     * referenced by this {@link WorkQuery}.
-     */
-    public abstract @NonNull ListenableFuture<List<WorkInfo>> getWorkInfos(
-            @NonNull WorkQuery workQuery);
-
-    /**
-     * Updates the work with the new specification. A {@link WorkRequest} passed as parameter
-     * must have an id set with {@link WorkRequest.Builder#setId(UUID)} that matches an id of the
-     * previously enqueued work.
-     * <p>
-     * It preserves enqueue time, e.g. if a work was enqueued 3 hours ago and had 6 hours long
-     * initial delay, after the update it would be still eligible for run in 3 hours, assuming
-     * that initial delay wasn't updated.
-     * <p>
-     * If the work being updated is currently running the returned ListenableFuture will be
-     * completed with {@link UpdateResult#APPLIED_FOR_NEXT_RUN}. In this case the current run won't
-     * be interrupted and will continue to rely on previous state of the request, e.g. using
-     * old constraints, tags etc. However, on the next run, e.g. retry of one-time Worker or
-     * another iteration of periodic worker, the new worker specification will be used.
-     * <p>
-     * If the one time work that is updated is already finished the returned ListenableFuture
-     * will be completed with {@link UpdateResult#NOT_APPLIED}.
-     * <p>
-     * If update can be applied immediately, e.g. the updated work isn't currently running,
-     * the returned ListenableFuture will be completed with
-     * {@link UpdateResult#APPLIED_IMMEDIATELY}.
-     * <p>
-     * If the work with the given id ({@code request.getId()}) doesn't exist the returned
-     * ListenableFuture will be completed exceptionally with {@link IllegalArgumentException}.
-     * <p>
-     * Worker type can't be changed, {@link OneTimeWorkRequest} can't be updated to
-     * {@link PeriodicWorkRequest} and otherwise, the returned ListenableFuture will be
-     * completed with {@link IllegalArgumentException}.
-     *
-     * @param request the new specification for the work.
-     * @return a {@link ListenableFuture} that will be successfully completed if the update was
-     * successful. The future will be completed with an exception if the work is already running
-     * or finished.
-     */
-    // consistent with already existent method like getWorkInfos() in WorkManager
-    @SuppressWarnings("AsyncSuffixFuture")
-    @NonNull
-    public abstract ListenableFuture<UpdateResult> updateWork(@NonNull WorkRequest request);
-
-    /**
-     * An enumeration of results for {@link WorkManager#updateWork(WorkRequest)} method.
-     */
-    public enum UpdateResult {
-        /**
-         * An update wasn't applied, because {@code Worker} has already finished.
-         */
-        NOT_APPLIED,
-        /**
-         * An update was successfully applied immediately, meaning
-         * the updated work wasn't currently running in the moment of the request.
-         * See {@link UpdateResult#APPLIED_FOR_NEXT_RUN} for the case of running worker.
-         */
-        APPLIED_IMMEDIATELY,
-        /**
-         * An update was successfully applied, but the worker being updated was running.
-         * This run isn't interrupted and will continue to rely on previous state of the
-         * request, e.g. using old constraints, tags etc. However, on the next run, e.g. retry of
-         * one-time Worker or another iteration of periodic worker, the new worker specification.
-         * will be used.
-         */
-        APPLIED_FOR_NEXT_RUN,
-    }
-
-    /**
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    protected WorkManager() {
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkManager.kt b/work/work-runtime/src/main/java/androidx/work/WorkManager.kt
new file mode 100644
index 0000000..e9be91f
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/WorkManager.kt
@@ -0,0 +1,686 @@
+/*
+ * 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.work
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.work.impl.WorkManagerImpl
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.UUID
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * WorkManager is the recommended library for persistent work.
+ * Scheduled work is guaranteed to execute sometime after its [Constraints] are met.
+ * WorkManager allows observation of work status and the ability to create complex chains of work.
+ *
+ * WorkManager uses an underlying job dispatching service when available based on the following
+ * criteria:
+ *
+ *  * Uses JobScheduler for API 23+
+ *  * Uses a custom AlarmManager + BroadcastReceiver implementation for API 14-22
+ *
+ * All work must be done in a [ListenableWorker] class.  A simple implementation,
+ * [Worker], is recommended as the starting point for most developers.  With the optional
+ * dependencies, you can also use `CoroutineWorker` or `RxWorker`.  All background work
+ * is given a maximum of ten minutes to finish its execution.  After this time has expired, the
+ * worker will be signalled to stop.
+ *
+ * There are two types of work supported by WorkManager: [OneTimeWorkRequest] and
+ * [PeriodicWorkRequest].  You can enqueue requests using WorkManager as follows:
+ *
+ * ```
+ * WorkManager workManager = WorkManager.getInstance(Context);
+ * workManager.enqueue(new OneTimeWorkRequest.Builder(FooWorker.class).build());
+ * ```
+ *
+ * A [WorkRequest] has an associated id that can be used for lookups and observation as
+ * follows:
+ *
+ * ```
+ * WorkRequest request = new OneTimeWorkRequest.Builder(FooWorker.class).build();
+ * workManager.enqueue(request);
+ * LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(request.getId());
+ * status.observe(...);
+ * ```
+ *
+ * You can also use the id for cancellation:
+ *
+ * ```
+ * WorkRequest request = new OneTimeWorkRequest.Builder(FooWorker.class).build();
+ * workManager.enqueue(request);
+ * workManager.cancelWorkById(request.getId());
+ * ```
+ *
+ * You can chain work as follows:
+ *
+ * ```
+ * WorkRequest request1 = new OneTimeWorkRequest.Builder(FooWorker.class).build();
+ * WorkRequest request2 = new OneTimeWorkRequest.Builder(BarWorker.class).build();
+ * WorkRequest request3 = new OneTimeWorkRequest.Builder(BazWorker.class).build();
+ * workManager.beginWith(request1, request2).then(request3).enqueue();
+ * ```
+ *
+ * Each call to [beginWith] returns a [WorkContinuation] upon which you can call
+ * [WorkContinuation.then] with a single [OneTimeWorkRequest] or a list of [OneTimeWorkRequest] to
+ * chain further work.  This allows for creation of complex chains of work.  For example, to create
+ * a chain like this:
+ *
+ * ```
+ *            A
+ *            |
+ *      +----------+
+ *      |          |
+ *      B          C
+ *      |
+ *   +----+
+ *   |    |
+ *   D    E
+ * ```
+ *
+ * you would enqueue them as follows:
+ *
+ * ```
+ * WorkContinuation continuation = workManager.beginWith(A);
+ * continuation.then(B).then(D, E).enqueue();  // A is implicitly enqueued here
+ * continuation.then(C).enqueue();
+ * ```
+ *
+ * Work is eligible for execution when all of its prerequisites are complete.  If any of its
+ * prerequisites fail or are cancelled, the work will never run.
+ *
+ * WorkRequests can accept [Constraints], inputs (see [Data]), and backoff criteria.
+ * WorkRequests can be tagged with human-readable Strings
+ * (see [WorkRequest.Builder.addTag]), and chains of work can be given a
+ * uniquely-identifiable name (see [beginUniqueWork]).
+ *
+ * ### Initializing WorkManager
+ *
+ * By default, WorkManager auto-initializes itself using a built-in `ContentProvider`.
+ * ContentProviders are created and run before the `Application` object, so this allows the
+ * WorkManager singleton to be setup before your code can run in most cases.  This is suitable for
+ * most developers.  However, you can provide a custom [Configuration] by using
+ * [Configuration.Provider] or [WorkManager.initialize].
+ *
+ * ### Renaming and Removing ListenableWorker Classes
+ *
+ * Exercise caution in renaming classes derived from [ListenableWorker]s. WorkManager stores
+ * the class name in its internal database when the [WorkRequest] is enqueued so it can later
+ * create an instance of that worker when constraints are met. Unless otherwise specified in the
+ * WorkManager [Configuration], this is done in the default [WorkerFactory] which tries
+ * to reflectively create the ListenableWorker object. Therefore, renaming or removing these
+ * classes is dangerous - if there is pending work with the given class, it will fail permanently
+ * if the class cannot be found.  If you are using a custom WorkerFactory, make sure you properly
+ * handle cases where the class is not found so that your code does not crash.
+ *
+ * In case it is desirable to rename a class, implement a custom WorkerFactory that instantiates the
+ * right ListenableWorker for the old class name.
+ */
+// Suppressing Metalava checks for added abstract methods in WorkManager.
+// WorkManager cannot be extended, because the constructor is marked @Restricted
+@SuppressLint("AddedAbstractMethod")
+abstract class WorkManager internal constructor() {
+
+    companion object {
+        /**
+         * Retrieves the `default` singleton instance of [WorkManager].
+         *
+         * @return The singleton instance of [WorkManager]; this may be `null` in unusual
+         * circumstances where you have disabled automatic initialization and have failed to
+         * manually call [initialize].
+         * @throws IllegalStateException If WorkManager is not initialized properly as per
+         * the exception message.
+         */
+        // `open` modifier was added to avoid errors in WorkManagerImpl:
+        // "WorkManagerImpl cannot override <X> in WorkManager", even though methods are static
+        @Suppress("NON_FINAL_MEMBER_IN_OBJECT")
+        @Deprecated(
+            message = "Use the overload receiving Context",
+            replaceWith = ReplaceWith("WorkManager.getContext(context)"),
+        )
+        @JvmStatic
+        open fun getInstance(): WorkManager {
+            @Suppress("DEPRECATION")
+            val workManager: WorkManager? = WorkManagerImpl.getInstance()
+            checkNotNull(workManager) {
+                "WorkManager is not initialized properly.  The most " +
+                    "likely cause is that you disabled WorkManagerInitializer in your manifest " +
+                    "but forgot to call WorkManager#initialize in your Application#onCreate or a " +
+                    "ContentProvider."
+            }
+            return workManager
+        }
+
+        /**
+         * Retrieves the `default` singleton instance of [WorkManager].
+         *
+         * @param context A [Context] for on-demand initialization.
+         * @return The singleton instance of [WorkManager]; this may be `null` in unusual
+         * circumstances where you have disabled automatic initialization and have failed to
+         * manually call [initialize].
+         * @throws IllegalStateException If WorkManager is not initialized properly
+         */
+        // `open` modifier was added to avoid errors in WorkManagerImpl:
+        // "WorkManagerImpl cannot override <X> in WorkManager", even though methods are static
+        @Suppress("NON_FINAL_MEMBER_IN_OBJECT")
+        @JvmStatic
+        open fun getInstance(context: Context): WorkManager {
+            return WorkManagerImpl.getInstance(context)
+        }
+
+        /**
+         * Used to do a one-time initialization of the [WorkManager] singleton with a custom
+         * [Configuration]. By default, this method should not be called because WorkManager is
+         * automatically initialized. To initialize WorkManager yourself, please follow these steps:
+         *
+         *  * Disable `androidx.work.WorkManagerInitializer` in your manifest.
+         *  * Invoke this method in `Application#onCreate` or a `ContentProvider`. Note
+         * that this method **must** be invoked in one of these two places or you risk getting a
+         * `NullPointerException` in [getInstance].
+         *
+         * This method throws an [IllegalStateException] when attempting to initialize in
+         * direct boot mode.
+         *
+         * This method throws an exception if it is called multiple times.
+         *
+         * @param context A [Context] object for configuration purposes. Internally, this class
+         * will call [Context.getApplicationContext], so you may safely pass in
+         * any Context without risking a memory leak.
+         * @param configuration The [Configuration] for used to set up WorkManager.
+         * @see Configuration.Provider for on-demand initialization.
+         */
+        // `open` modifier was added to avoid errors in WorkManagerImpl:
+        // "WorkManagerImpl cannot override <X> in WorkManager", even though methods are static
+        @Suppress("NON_FINAL_MEMBER_IN_OBJECT")
+        @JvmStatic
+        open fun initialize(context: Context, configuration: Configuration) {
+            WorkManagerImpl.initialize(context, configuration)
+        }
+
+        /**
+         * Provides a way to check if [WorkManager] is initialized in this process.
+         *
+         * @return `true` if [WorkManager] has been initialized in this process.
+         */
+        @Suppress("NON_FINAL_MEMBER_IN_OBJECT")
+        @JvmStatic
+        open fun isInitialized(): Boolean = WorkManagerImpl.isInitialized()
+    }
+
+    /**
+     * The [Configuration] instance that [WorkManager] was initialized with.
+     */
+    abstract val configuration: Configuration
+
+    /**
+     * Enqueues one item for background processing.
+     *
+     * @param request The [WorkRequest] to enqueue
+     * @return An [Operation] that can be used to determine when the enqueue has completed
+     */
+    fun enqueue(request: WorkRequest): Operation {
+        return enqueue(listOf(request))
+    }
+
+    /**
+     * Enqueues one or more items for background processing.
+     *
+     * @param requests One or more [WorkRequest] to enqueue
+     * @return An [Operation] that can be used to determine when the enqueue has completed
+     */
+    abstract fun enqueue(requests: List<WorkRequest>): Operation
+
+    /**
+     * Begins a chain with one or more [OneTimeWorkRequest]s, which can be enqueued together
+     * in the future using [WorkContinuation.enqueue].
+     *
+     * If any work in the chain fails or is cancelled, all of its dependent work inherits that state
+     * and will never run.
+     *
+     * @param request One or more [OneTimeWorkRequest] to start a chain of work
+     * @return A [WorkContinuation] that allows for further chaining of dependent
+     * [OneTimeWorkRequest]
+     */
+    fun beginWith(request: OneTimeWorkRequest): WorkContinuation {
+        return beginWith(listOf(request))
+    }
+
+    /**
+     * Begins a chain with one or more [OneTimeWorkRequest]s, which can be enqueued together
+     * in the future using [WorkContinuation.enqueue].
+     *
+     * If any work in the chain fails or is cancelled, all of its dependent work inherits that state
+     * and will never run.
+     *
+     * @param requests One or more [OneTimeWorkRequest] to start a chain of work
+     * @return A [WorkContinuation] that allows for further chaining of dependent
+     * [OneTimeWorkRequest]
+     */
+    abstract fun beginWith(requests: List<OneTimeWorkRequest>): WorkContinuation
+
+    /**
+     * This method allows you to begin unique chains of work for situations where you only want one
+     * chain with a given name to be active at a time.  For example, you may only want one sync
+     * operation to be active.  If there is one pending, you can choose to let it run or replace it
+     * with your new work.
+     *
+     * The `uniqueWorkName` uniquely identifies this set of work.
+     *
+     * If this method determines that new work should be enqueued and run, all records of previous
+     * work with `uniqueWorkName` will be pruned.  If this method determines that new work
+     * should NOT be run, then the entire chain will be considered a no-op.
+     *
+     * If any work in the chain fails or is cancelled, all of its dependent work inherits that state
+     * and will never run.  This is particularly important if you are using `APPEND` as your
+     * [ExistingWorkPolicy].
+     *
+     * @param uniqueWorkName A unique name which for this chain of work
+     * @param existingWorkPolicy An [ExistingWorkPolicy]
+     * @param request The [OneTimeWorkRequest] to enqueue. `REPLACE` ensures that if there
+     * is pending work labelled with `uniqueWorkName`, it will be cancelled and
+     * the new work will run. `KEEP` will run the new sequence of work only if
+     * there is no pending work labelled with `uniqueWorkName`.  `APPEND`
+     * will create a new sequence of work if there is no existing work with
+     * `uniqueWorkName`; otherwise, `work` will be added as a child of all
+     * leaf nodes labelled with `uniqueWorkName`.
+     * @return A [WorkContinuation] that allows further chaining
+     */
+    fun beginUniqueWork(
+        uniqueWorkName: String,
+        existingWorkPolicy: ExistingWorkPolicy,
+        request: OneTimeWorkRequest
+    ): WorkContinuation {
+        return beginUniqueWork(uniqueWorkName, existingWorkPolicy, listOf(request))
+    }
+
+    /**
+     * This method allows you to begin unique chains of work for situations where you only want one
+     * chain with a given name to be active at a time.  For example, you may only want one sync
+     * operation to be active.  If there is one pending, you can choose to let it run or replace it
+     * with your new work.
+     *
+     * The `uniqueWorkName` uniquely identifies this set of work.
+     *
+     * If this method determines that new work should be enqueued and run, all records of previous
+     * work with `uniqueWorkName` will be pruned.  If this method determines that new work
+     * should NOT be run, then the entire chain will be considered a no-op.
+     *
+     * If any work in the chain fails or is cancelled, all of its dependent work inherits that state
+     * and will never run.  This is particularly important if you are using `APPEND` as your
+     * [ExistingWorkPolicy].
+     *
+     * @param uniqueWorkName A unique name which for this chain of work
+     * @param existingWorkPolicy An [ExistingWorkPolicy]; see below for more information
+     * @param requests One or more [OneTimeWorkRequest] to enqueue. `REPLACE` ensures that
+     * if there is pending work labelled with `uniqueWorkName`, it will be
+     * cancelled and the new work will run. `KEEP` will run the new sequence of
+     * work only if there is no pending work labelled with `uniqueWorkName`.
+     * `APPEND` will create a new sequence of work if there is no
+     * existing work with `uniqueWorkName`; otherwise, `work` will be added
+     * as a child of all leaf nodes labelled with `uniqueWorkName`.
+     * @return A [WorkContinuation] that allows further chaining
+     */
+    abstract fun beginUniqueWork(
+        uniqueWorkName: String,
+        existingWorkPolicy: ExistingWorkPolicy,
+        requests: List<OneTimeWorkRequest>
+    ): WorkContinuation
+
+    /**
+     * This method allows you to enqueue `work` requests to a uniquely-named
+     * [WorkContinuation], where only one continuation of a particular name can be active at
+     * a time. For example, you may only want one sync operation to be active. If there is one
+     * pending, you can choose to let it run or replace it with your new work.
+     *
+     * The `uniqueWorkName` uniquely identifies this [WorkContinuation].
+     *
+     * @param uniqueWorkName A unique name which for this operation
+     * @param existingWorkPolicy An [ExistingWorkPolicy]; see below for more information
+     * @param request The [OneTimeWorkRequest]s to enqueue. `REPLACE` ensures that if there
+     * is pending work labelled with `uniqueWorkName`, it will be cancelled and
+     * the new work will run. `KEEP` will run the new OneTimeWorkRequests only if
+     * there is no pending work labelled with `uniqueWorkName`.  `APPEND`
+     * will append the OneTimeWorkRequests as leaf nodes labelled with
+     * `uniqueWorkName`.
+     * @return An [Operation] that can be used to determine when the enqueue has completed
+     */
+    open fun enqueueUniqueWork(
+        uniqueWorkName: String,
+        existingWorkPolicy: ExistingWorkPolicy,
+        request: OneTimeWorkRequest
+    ): Operation {
+        return enqueueUniqueWork(uniqueWorkName, existingWorkPolicy, listOf(request))
+    }
+
+    /**
+     * This method allows you to enqueue `work` requests to a uniquely-named
+     * [WorkContinuation], where only one continuation of a particular name can be active at
+     * a time. For example, you may only want one sync operation to be active. If there is one
+     * pending, you can choose to let it run or replace it with your new work.
+     *
+     * The `uniqueWorkName` uniquely identifies this [WorkContinuation].
+     *
+     * @param uniqueWorkName A unique name which for this operation
+     * @param existingWorkPolicy An [ExistingWorkPolicy]
+     * @param requests [OneTimeWorkRequest]s to enqueue. `REPLACE` ensures
+     * that if there is pending work labelled with `uniqueWorkName`, it
+     * will be cancelled and the new work will run. `KEEP` will run the
+     * new OneTimeWorkRequests only if there is no pending work labelled with
+     * `uniqueWorkName`. `APPEND` will append the
+     * OneTimeWorkRequests as leaf nodes labelled with `uniqueWorkName`.
+     * @return An [Operation] that can be used to determine when the enqueue has completed
+     */
+    abstract fun enqueueUniqueWork(
+        uniqueWorkName: String,
+        existingWorkPolicy: ExistingWorkPolicy,
+        requests: List<OneTimeWorkRequest>
+    ): Operation
+
+    /**
+     * This method allows you to enqueue a uniquely-named [PeriodicWorkRequest], where only
+     * one PeriodicWorkRequest of a particular name can be active at a time.  For example, you may
+     * only want one sync operation to be active.  If there is one pending, you can choose to let it
+     * run or replace it with your new work.
+     *
+     * The `uniqueWorkName` uniquely identifies this PeriodicWorkRequest.
+     *
+     * @param uniqueWorkName A unique name which for this operation
+     * @param existingPeriodicWorkPolicy An [ExistingPeriodicWorkPolicy]
+     * @param request A [PeriodicWorkRequest] to enqueue. `REPLACE` ensures that if
+     * there is pending work labelled with `uniqueWorkName`, it will be
+     * cancelled and the new work will run. `KEEP` will run the new
+     * PeriodicWorkRequest only if there is no pending work labelled with
+     * `uniqueWorkName`.
+     * @return An [Operation] that can be used to determine when the enqueue has completed
+     */
+    abstract fun enqueueUniquePeriodicWork(
+        uniqueWorkName: String,
+        existingPeriodicWorkPolicy: ExistingPeriodicWorkPolicy,
+        request: PeriodicWorkRequest
+    ): Operation
+
+    /**
+     * Cancels work with the given id if it isn't finished.  Note that cancellation is a best-effort
+     * policy and work that is already executing may continue to run. Upon cancellation,
+     * [ListenableFuture] returned by [ListenableWorker.startWork] will be cancelled. Also
+     * [ListenableWorker.onStopped] will be invoked for any affected workers.
+     *
+     * @param id The id of the work
+     * @return An [Operation] that can be used to determine when the cancelWorkById has
+     * completed
+     */
+    abstract fun cancelWorkById(id: UUID): Operation
+
+    /**
+     * Cancels all unfinished work with the given tag.  Note that cancellation is a best-effort
+     * policy and work that is already executing may continue to run. Upon cancellation,
+     * [ListenableFuture] returned by [ListenableWorker.startWork] will be cancelled. Also
+     * [ListenableWorker.onStopped] will be invoked for any affected workers.
+     *
+     * @param tag The tag used to identify the work
+     * @return An [Operation] that can be used to determine when the cancelAllWorkByTag has
+     * completed
+     */
+    abstract fun cancelAllWorkByTag(tag: String): Operation
+
+    /**
+     * Cancels all unfinished work in the work chain with the given name.  Note that cancellation is
+     * a best-effort policy and work that is already executing may continue to run. Upon
+     * cancellation, [ListenableFuture] returned by [ListenableWorker.startWork] will be cancelled.
+     * Also [ListenableWorker.onStopped] will be invoked for any affected workers.
+     *
+     * @param uniqueWorkName The unique name used to identify the chain of work
+     * @return An [Operation] that can be used to determine when the cancelUniqueWork has
+     * completed
+     */
+    abstract fun cancelUniqueWork(uniqueWorkName: String): Operation
+
+    /**
+     * Cancels all unfinished work.  **Use this method with extreme caution!**  By invoking it,
+     * you will potentially affect other modules or libraries in your codebase.  It is strongly
+     * recommended that you use one of the other cancellation methods at your disposal.
+     *
+     * Upon cancellation, [ListenableFuture] returned by [ListenableWorker.startWork] will be
+     * cancelled. Also [ListenableWorker.onStopped] will be invoked for any affected workers.
+     *
+     * @return An [Operation] that can be used to determine when the cancelAllWork has
+     * completed
+     */
+    abstract fun cancelAllWork(): Operation
+
+    /**
+     * Creates a [PendingIntent] which can be used to cancel a [WorkRequest] with the
+     * given `id`.
+     *
+     * @param id      The [WorkRequest] id.
+     * @return The [PendingIntent] that can be used to cancel the [WorkRequest].
+     */
+    abstract fun createCancelPendingIntent(id: UUID): PendingIntent
+
+    /**
+     * Prunes all eligible finished work from the internal database.  Eligible work must be finished
+     * ([WorkInfo.State.SUCCEEDED], [WorkInfo.State.FAILED], or
+     * [WorkInfo.State.CANCELLED]), with zero unfinished dependents.
+     *
+     * **Use this method with caution**; by invoking it, you (and any modules and libraries in
+     * your codebase) will no longer be able to observe the [WorkInfo] of the pruned work.
+     * You do not normally need to call this method - WorkManager takes care to auto-prune its work
+     * after a sane period of time.  This method also ignores the
+     * [OneTimeWorkRequest.Builder.keepResultsForAtLeast] policy.
+     *
+     * @return An [Operation] that can be used to determine when the pruneWork has
+     * completed
+     */
+    abstract fun pruneWork(): Operation
+
+    /**
+     * Gets a [LiveData] of the last time all work was cancelled.  This method is intended for
+     * use by library and module developers who have dependent data in their own repository that
+     * must be updated or deleted in case someone cancels their work without their prior knowledge.
+     *
+     * @return A [LiveData] of the timestamp (`System#getCurrentTimeMillis()`) when
+     * [cancelAllWork] was last invoked; this timestamp may be `0L` if this
+     * never occurred
+     */
+    abstract fun getLastCancelAllTimeMillisLiveData(): LiveData<Long>
+
+    /**
+     * Gets a [ListenableFuture] of the last time all work was cancelled.  This method is
+     * intended for use by library and module developers who have dependent data in their own
+     * repository that must be updated or deleted in case someone cancels their work without
+     * their prior knowledge.
+     *
+     * @return A [ListenableFuture] of the timestamp (`System#getCurrentTimeMillis()`)
+     * when [cancelAllWork] was last invoked; this timestamp may be `0L` if
+     * this never occurred
+     */
+    abstract fun getLastCancelAllTimeMillis(): ListenableFuture<Long>
+
+    /**
+     * Gets a [LiveData] of the [WorkInfo] for a given work id.
+     *
+     * @param id The id of the work
+     * @return A [LiveData] of the [WorkInfo] associated with `id`; note that
+     * this [WorkInfo] may be `null` if `id` is not known to WorkManager.
+     */
+    abstract fun getWorkInfoByIdLiveData(id: UUID): LiveData<WorkInfo?>
+
+    /**
+     * Gets a [Flow] of the [WorkInfo] for a given work id.
+     *
+     * @param id The id of the work
+     * @return A [Flow] of the [WorkInfo] associated with `id`; note that
+     * this [WorkInfo] may be `null` if `id` is not known to WorkManager.
+     */
+    abstract fun getWorkInfoByIdFlow(id: UUID): Flow<WorkInfo?>
+
+    /**
+     * Gets a [ListenableFuture] of the [WorkInfo] for a given work id.
+     *
+     * @param id The id of the work
+     * @return A [ListenableFuture] of the [WorkInfo] associated with `id`;
+     * note that this [WorkInfo] may be `null` if `id` is not known to WorkManager
+     */
+    abstract fun getWorkInfoById(id: UUID): ListenableFuture<WorkInfo?>
+
+    /**
+     * Gets a [LiveData] of the [WorkInfo] for all work for a given tag.
+     *
+     * @param tag The tag of the work
+     * @return A [LiveData] list of [WorkInfo] for work tagged with `tag`
+     */
+    abstract fun getWorkInfosByTagLiveData(tag: String): LiveData<List<WorkInfo>>
+
+    /**
+     * Gets a [Flow] of the [WorkInfo] for all work for a given tag.
+     *
+     * @param tag The tag of the work
+     * @return A [Flow] list of [WorkInfo] for work tagged with `tag`
+     */
+    abstract fun getWorkInfosByTagFlow(tag: String): Flow<List<WorkInfo>>
+
+    /**
+     * Gets a [ListenableFuture] of the [WorkInfo] for all work for a given tag.
+     *
+     * @param tag The tag of the work
+     * @return A [ListenableFuture] list of [WorkInfo] for work tagged with `tag`
+     */
+    abstract fun getWorkInfosByTag(tag: String): ListenableFuture<List<WorkInfo>>
+
+    /**
+     * Gets a [LiveData] of the [WorkInfo] for all work in a work chain with a given
+     * unique name.
+     *
+     * @param uniqueWorkName The unique name used to identify the chain of work
+     * @return A [LiveData] of the [WorkInfo] for work in the chain named `uniqueWorkName`
+     */
+    abstract fun getWorkInfosForUniqueWorkLiveData(uniqueWorkName: String): LiveData<List<WorkInfo>>
+
+    /**
+     * Gets a [Flow] of the [WorkInfo] for all work in a work chain with a given
+     * unique name.
+     *
+     * @param uniqueWorkName The unique name used to identify the chain of work
+     * @return A [Flow] of the [WorkInfo] for work in the chain named `uniqueWorkName`
+     */
+    abstract fun getWorkInfosForUniqueWorkFlow(uniqueWorkName: String): Flow<List<WorkInfo>>
+
+    /**
+     * Gets a [ListenableFuture] of the [WorkInfo] for all work in a work chain
+     * with a given unique name.
+     *
+     * @param uniqueWorkName The unique name used to identify the chain of work
+     * @return A [ListenableFuture] of the [WorkInfo] for work in the chain named `uniqueWorkName`
+     */
+    abstract fun getWorkInfosForUniqueWork(uniqueWorkName: String): ListenableFuture<List<WorkInfo>>
+
+    /**
+     * Gets the [LiveData] of the [List] of [WorkInfo] for all work
+     * referenced by the [WorkQuery] specification.
+     *
+     * @param workQuery The work query specification
+     * @return A [LiveData] of the [List] of [WorkInfo] for work
+     * referenced by this [WorkQuery].
+     */
+    abstract fun getWorkInfosLiveData(workQuery: WorkQuery): LiveData<List<WorkInfo>>
+
+    /**
+     * Gets the [Flow] of the [List] of [WorkInfo] for all work
+     * referenced by the [WorkQuery] specification.
+     *
+     * @param workQuery The work query specification
+     * @return A [Flow] of the [List] of [WorkInfo] for work
+     * referenced by this [WorkQuery].
+     */
+    abstract fun getWorkInfosFlow(workQuery: WorkQuery): Flow<List<WorkInfo>>
+
+    /**
+     * Gets the [ListenableFuture] of the [List] of [WorkInfo] for all work
+     * referenced by the [WorkQuery] specification.
+     *
+     * @param workQuery The work query specification
+     * @return A [ListenableFuture] of the [List] of [WorkInfo] for work
+     * referenced by this [WorkQuery].
+     */
+    abstract fun getWorkInfos(workQuery: WorkQuery): ListenableFuture<List<WorkInfo>>
+
+    /**
+     * Updates the work with the new specification. A [WorkRequest] passed as parameter
+     * must have an id set with [WorkRequest.Builder.setId] that matches an id of the
+     * previously enqueued work.
+     *
+     * It preserves enqueue time, e.g. if a work was enqueued 3 hours ago and had 6 hours long
+     * initial delay, after the update it would be still eligible for run in 3 hours, assuming
+     * that initial delay wasn't updated.
+     *
+     * If the work being updated is currently running the returned ListenableFuture will be
+     * completed with [UpdateResult.APPLIED_FOR_NEXT_RUN]. In this case the current run won't
+     * be interrupted and will continue to rely on previous state of the request, e.g. using
+     * old constraints, tags etc. However, on the next run, e.g. retry of one-time Worker or
+     * another iteration of periodic worker, the new worker specification will be used.
+     *
+     * If the one time work that is updated is already finished the returned ListenableFuture
+     * will be completed with [UpdateResult.NOT_APPLIED].
+     *
+     * If update can be applied immediately, e.g. the updated work isn't currently running,
+     * the returned ListenableFuture will be completed with
+     * [UpdateResult.APPLIED_IMMEDIATELY].
+     *
+     * If the work with the given id (`request.getId()`) doesn't exist the returned
+     * ListenableFuture will be completed exceptionally with [IllegalArgumentException].
+     *
+     * Worker type can't be changed, [OneTimeWorkRequest] can't be updated to
+     * [PeriodicWorkRequest] and otherwise, the returned ListenableFuture will be
+     * completed with [IllegalArgumentException].
+     *
+     * @param request the new specification for the work.
+     * @return a [ListenableFuture] that will be successfully completed if the update was
+     * successful. The future will be completed with an exception if the work is already running
+     * or finished.
+     */
+    // consistent with already existent method like getWorkInfos() in WorkManager
+    @Suppress("AsyncSuffixFuture")
+    abstract fun updateWork(request: WorkRequest): ListenableFuture<UpdateResult>
+
+    /**
+     * An enumeration of results for [WorkManager.updateWork] method.
+     */
+    enum class UpdateResult {
+        /**
+         * An update wasn't applied, because `Worker` has already finished.
+         */
+        NOT_APPLIED,
+
+        /**
+         * An update was successfully applied immediately, meaning
+         * the updated work wasn't currently running in the moment of the request.
+         * See [UpdateResult.APPLIED_FOR_NEXT_RUN] for the case of running worker.
+         */
+        APPLIED_IMMEDIATELY,
+
+        /**
+         * An update was successfully applied, but the worker being updated was running.
+         * This run isn't interrupted and will continue to rely on previous state of the
+         * request, e.g. using old constraints, tags etc. However, on the next run, e.g. retry of
+         * one-time Worker or another iteration of periodic worker, the new worker specification.
+         * will be used.
+         */
+        APPLIED_FOR_NEXT_RUN,
+    }
+}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index 1ff6900..b2d4e6c 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -351,32 +351,33 @@
     }
 
     @Override
-    public @NonNull WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work) {
-        if (work.isEmpty()) {
+    public @NonNull WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> requests) {
+        if (requests.isEmpty()) {
             throw new IllegalArgumentException(
                     "beginWith needs at least one OneTimeWorkRequest.");
         }
-        return new WorkContinuationImpl(this, work);
+        return new WorkContinuationImpl(this, requests);
     }
 
     @Override
     public @NonNull WorkContinuation beginUniqueWork(
             @NonNull String uniqueWorkName,
             @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull List<OneTimeWorkRequest> work) {
-        if (work.isEmpty()) {
+            @NonNull List<OneTimeWorkRequest> requests) {
+        if (requests.isEmpty()) {
             throw new IllegalArgumentException(
                     "beginUniqueWork needs at least one OneTimeWorkRequest.");
         }
-        return new WorkContinuationImpl(this, uniqueWorkName, existingWorkPolicy, work);
+        return new WorkContinuationImpl(this, uniqueWorkName, existingWorkPolicy, requests);
     }
 
     @NonNull
     @Override
     public Operation enqueueUniqueWork(@NonNull String uniqueWorkName,
             @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull List<OneTimeWorkRequest> work) {
-        return new WorkContinuationImpl(this, uniqueWorkName, existingWorkPolicy, work).enqueue();
+            @NonNull List<OneTimeWorkRequest> requests) {
+        return new WorkContinuationImpl(this, uniqueWorkName,
+                existingWorkPolicy, requests).enqueue();
     }
 
     @Override
@@ -384,14 +385,14 @@
     public Operation enqueueUniquePeriodicWork(
             @NonNull String uniqueWorkName,
             @NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
-            @NonNull PeriodicWorkRequest periodicWork) {
+            @NonNull PeriodicWorkRequest request) {
         if (existingPeriodicWorkPolicy == ExistingPeriodicWorkPolicy.UPDATE) {
-            return enqueueUniquelyNamedPeriodic(this, uniqueWorkName, periodicWork);
+            return enqueueUniquelyNamedPeriodic(this, uniqueWorkName, request);
         }
         return createWorkContinuationForUniquePeriodicWork(
                 uniqueWorkName,
                 existingPeriodicWorkPolicy,
-                periodicWork)
+                request)
                 .enqueue();
     }
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt
index 1c15845..9a91fe4 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt
@@ -364,7 +364,7 @@
      * @return The time at which the [WorkSpec] was scheduled.
      */
     @Query("SELECT schedule_requested_at FROM workspec WHERE id=:id")
-    fun getScheduleRequestedAtLiveData(id: String): LiveData<Long>
+    fun getScheduleRequestedAtLiveData(id: String): LiveData<Long?>
 
     /**
      * Resets the scheduled state on the [WorkSpec]s that are not in a a completed state.
diff --git a/work/work-testing/api/current.txt b/work/work-testing/api/current.txt
index 9361e19..6af87ec 100644
--- a/work/work-testing/api/current.txt
+++ b/work/work-testing/api/current.txt
@@ -32,7 +32,7 @@
     method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W> TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<java.lang.String> triggeredContentAuthorities);
   }
 
-  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W!> {
     method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker!> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
     method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
   }
diff --git a/work/work-testing/api/restricted_current.txt b/work/work-testing/api/restricted_current.txt
index 9361e19..6af87ec 100644
--- a/work/work-testing/api/restricted_current.txt
+++ b/work/work-testing/api/restricted_current.txt
@@ -32,7 +32,7 @@
     method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W> TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<java.lang.String> triggeredContentAuthorities);
   }
 
-  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W!> {
     method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker!> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
     method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
   }
diff --git a/work/work-testing/src/androidTest/java/androidx/work/testing/CustomClockTest.kt b/work/work-testing/src/androidTest/java/androidx/work/testing/CustomClockTest.kt
index 002e439..f594704 100644
--- a/work/work-testing/src/androidTest/java/androidx/work/testing/CustomClockTest.kt
+++ b/work/work-testing/src/androidTest/java/androidx/work/testing/CustomClockTest.kt
@@ -72,7 +72,7 @@
         workManagerImpl.enqueue(listOf(request)).result.get()
 
         val status = workManagerImpl.getWorkInfoById(request.id).get()
-        assertThat(status.nextScheduleTimeMillis)
+        assertThat(status!!.nextScheduleTimeMillis)
             .isEqualTo(testClock.timeMillis + initialDelay.toMillis())
     }
 }
diff --git a/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerRealExecutorTest.kt b/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerRealExecutorTest.kt
index 2089353..8a10c71 100644
--- a/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerRealExecutorTest.kt
+++ b/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerRealExecutorTest.kt
@@ -187,7 +187,7 @@
         }
 
         drainSerialExecutor()
-        assertThat(wm.getWorkInfoById(request.id).get().state).isEqualTo(ENQUEUED)
+        assertThat(wm.getWorkInfoById(request.id).get()!!.state).isEqualTo(ENQUEUED)
         assertThat(CountingTestWorker.COUNT.get()).isEqualTo(1)
     }
 
diff --git a/work/work-testing/src/test/java/androidx/work/testing/TestCoroutineSchedulerTest.kt b/work/work-testing/src/test/java/androidx/work/testing/TestCoroutineSchedulerTest.kt
index f58bf28..53b0595 100644
--- a/work/work-testing/src/test/java/androidx/work/testing/TestCoroutineSchedulerTest.kt
+++ b/work/work-testing/src/test/java/androidx/work/testing/TestCoroutineSchedulerTest.kt
@@ -112,7 +112,7 @@
         val workInfoEarly = workManager.getWorkInfoById(request.id)
         synchronizeThreads()
 
-        assertThat(Futures.getDone(workInfoEarly).state).isEqualTo(ENQUEUED)
+        assertThat(Futures.getDone(workInfoEarly)!!.state).isEqualTo(ENQUEUED)
 
         // Work should run and finish now.
         testCoroutineScheduler.advanceTimeBy(initialDelayMillis)
@@ -121,7 +121,7 @@
         // Verify result
         val workInfoOnTime = workManager.getWorkInfoById(request.id)
         synchronizeThreads()
-        assertThat(Futures.getDone(workInfoOnTime).state).isEqualTo(SUCCEEDED)
+        assertThat(Futures.getDone(workInfoOnTime)!!.state).isEqualTo(SUCCEEDED)
     }
 
     @Test
@@ -138,7 +138,7 @@
         val workInfoEarly = workManager.getWorkInfoById(request.id)
         synchronizeThreads()
 
-        assertThat(Futures.getDone(workInfoEarly).state).isEqualTo(ENQUEUED)
+        assertThat(Futures.getDone(workInfoEarly)!!.state).isEqualTo(ENQUEUED)
 
         // Can't use setXDelayMet with clock-based scheduling
         assertThrows(IllegalStateException::class.java) {