Merge "[Material3][Color] Add benchmark testing for ColorScheme" into androidx-main
diff --git a/activity/activity-compose/api/1.8.0-beta01.txt b/activity/activity-compose/api/1.8.0-beta01.txt
new file mode 100644
index 0000000..5601568
--- /dev/null
+++ b/activity/activity-compose/api/1.8.0-beta01.txt
@@ -0,0 +1,54 @@
+// Signature format: 4.0
+package androidx.activity.compose {
+
+  public final class ActivityResultRegistryKt {
+    method @androidx.compose.runtime.Composable public static <I, O> androidx.activity.compose.ManagedActivityResultLauncher<I,O> rememberLauncherForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, kotlin.jvm.functions.Function1<? super O,kotlin.Unit> onResult);
+  }
+
+  public final class BackHandlerKt {
+    method @androidx.compose.runtime.Composable public static void BackHandler(optional boolean enabled, kotlin.jvm.functions.Function0<kotlin.Unit> onBack);
+  }
+
+  public final class ComponentActivityKt {
+    method public static void setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionContext? parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public final class LocalActivityResultRegistryOwner {
+    method @androidx.compose.runtime.Composable public androidx.activity.result.ActivityResultRegistryOwner? getCurrent();
+    method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.result.ActivityResultRegistryOwner> provides(androidx.activity.result.ActivityResultRegistryOwner registryOwner);
+    property @androidx.compose.runtime.Composable public final androidx.activity.result.ActivityResultRegistryOwner? current;
+    field public static final androidx.activity.compose.LocalActivityResultRegistryOwner INSTANCE;
+  }
+
+  public final class LocalFullyDrawnReporterOwner {
+    method @androidx.compose.runtime.Composable public androidx.activity.FullyDrawnReporterOwner? getCurrent();
+    method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.FullyDrawnReporterOwner> provides(androidx.activity.FullyDrawnReporterOwner fullyDrawnReporterOwner);
+    property @androidx.compose.runtime.Composable public final androidx.activity.FullyDrawnReporterOwner? current;
+    field public static final androidx.activity.compose.LocalFullyDrawnReporterOwner INSTANCE;
+  }
+
+  public final class LocalOnBackPressedDispatcherOwner {
+    method @androidx.compose.runtime.Composable public androidx.activity.OnBackPressedDispatcherOwner? getCurrent();
+    method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.OnBackPressedDispatcherOwner> provides(androidx.activity.OnBackPressedDispatcherOwner dispatcherOwner);
+    property @androidx.compose.runtime.Composable public final androidx.activity.OnBackPressedDispatcherOwner? current;
+    field public static final androidx.activity.compose.LocalOnBackPressedDispatcherOwner INSTANCE;
+  }
+
+  public final class ManagedActivityResultLauncher<I, O> extends androidx.activity.result.ActivityResultLauncher<I> {
+    method public androidx.activity.result.contract.ActivityResultContract<I,?> getContract();
+    method public void launch(I input, androidx.core.app.ActivityOptionsCompat? options);
+    method @Deprecated public void unregister();
+  }
+
+  public final class PredictiveBackHandlerKt {
+    method @androidx.compose.runtime.Composable public static void PredictiveBackHandler(optional boolean enabled, kotlin.jvm.functions.Function2<kotlinx.coroutines.flow.Flow<androidx.activity.BackEventCompat>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> onBack);
+  }
+
+  public final class ReportDrawnKt {
+    method @androidx.compose.runtime.Composable public static void ReportDrawn();
+    method @androidx.compose.runtime.Composable public static void ReportDrawnAfter(kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+    method @androidx.compose.runtime.Composable public static void ReportDrawnWhen(kotlin.jvm.functions.Function0<java.lang.Boolean> predicate);
+  }
+
+}
+
diff --git a/activity/activity-compose/api/res-1.8.0-beta01.txt b/activity/activity-compose/api/res-1.8.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/activity/activity-compose/api/res-1.8.0-beta01.txt
diff --git a/activity/activity-compose/api/restricted_1.8.0-beta01.txt b/activity/activity-compose/api/restricted_1.8.0-beta01.txt
new file mode 100644
index 0000000..5601568
--- /dev/null
+++ b/activity/activity-compose/api/restricted_1.8.0-beta01.txt
@@ -0,0 +1,54 @@
+// Signature format: 4.0
+package androidx.activity.compose {
+
+  public final class ActivityResultRegistryKt {
+    method @androidx.compose.runtime.Composable public static <I, O> androidx.activity.compose.ManagedActivityResultLauncher<I,O> rememberLauncherForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, kotlin.jvm.functions.Function1<? super O,kotlin.Unit> onResult);
+  }
+
+  public final class BackHandlerKt {
+    method @androidx.compose.runtime.Composable public static void BackHandler(optional boolean enabled, kotlin.jvm.functions.Function0<kotlin.Unit> onBack);
+  }
+
+  public final class ComponentActivityKt {
+    method public static void setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionContext? parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public final class LocalActivityResultRegistryOwner {
+    method @androidx.compose.runtime.Composable public androidx.activity.result.ActivityResultRegistryOwner? getCurrent();
+    method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.result.ActivityResultRegistryOwner> provides(androidx.activity.result.ActivityResultRegistryOwner registryOwner);
+    property @androidx.compose.runtime.Composable public final androidx.activity.result.ActivityResultRegistryOwner? current;
+    field public static final androidx.activity.compose.LocalActivityResultRegistryOwner INSTANCE;
+  }
+
+  public final class LocalFullyDrawnReporterOwner {
+    method @androidx.compose.runtime.Composable public androidx.activity.FullyDrawnReporterOwner? getCurrent();
+    method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.FullyDrawnReporterOwner> provides(androidx.activity.FullyDrawnReporterOwner fullyDrawnReporterOwner);
+    property @androidx.compose.runtime.Composable public final androidx.activity.FullyDrawnReporterOwner? current;
+    field public static final androidx.activity.compose.LocalFullyDrawnReporterOwner INSTANCE;
+  }
+
+  public final class LocalOnBackPressedDispatcherOwner {
+    method @androidx.compose.runtime.Composable public androidx.activity.OnBackPressedDispatcherOwner? getCurrent();
+    method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.OnBackPressedDispatcherOwner> provides(androidx.activity.OnBackPressedDispatcherOwner dispatcherOwner);
+    property @androidx.compose.runtime.Composable public final androidx.activity.OnBackPressedDispatcherOwner? current;
+    field public static final androidx.activity.compose.LocalOnBackPressedDispatcherOwner INSTANCE;
+  }
+
+  public final class ManagedActivityResultLauncher<I, O> extends androidx.activity.result.ActivityResultLauncher<I> {
+    method public androidx.activity.result.contract.ActivityResultContract<I,?> getContract();
+    method public void launch(I input, androidx.core.app.ActivityOptionsCompat? options);
+    method @Deprecated public void unregister();
+  }
+
+  public final class PredictiveBackHandlerKt {
+    method @androidx.compose.runtime.Composable public static void PredictiveBackHandler(optional boolean enabled, kotlin.jvm.functions.Function2<kotlinx.coroutines.flow.Flow<androidx.activity.BackEventCompat>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> onBack);
+  }
+
+  public final class ReportDrawnKt {
+    method @androidx.compose.runtime.Composable public static void ReportDrawn();
+    method @androidx.compose.runtime.Composable public static void ReportDrawnAfter(kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+    method @androidx.compose.runtime.Composable public static void ReportDrawnWhen(kotlin.jvm.functions.Function0<java.lang.Boolean> predicate);
+  }
+
+}
+
diff --git a/activity/activity-ktx/api/1.8.0-beta01.txt b/activity/activity-ktx/api/1.8.0-beta01.txt
new file mode 100644
index 0000000..7b96bf7
--- /dev/null
+++ b/activity/activity-ktx/api/1.8.0-beta01.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.activity {
+
+  public final class ActivityViewModelLazyKt {
+    method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+    method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+  }
+
+  public final class PipHintTrackerKt {
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static suspend Object? trackPipAnimationHintView(android.app.Activity, android.view.View view, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  }
+
+}
+
+package androidx.activity.result {
+
+  public final class ActivityResultCallerKt {
+    method public static <I, O> androidx.activity.result.ActivityResultLauncher<kotlin.Unit> registerForActivityResult(androidx.activity.result.ActivityResultCaller, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, androidx.activity.result.ActivityResultRegistry registry, kotlin.jvm.functions.Function1<? super O,kotlin.Unit> callback);
+    method public static <I, O> androidx.activity.result.ActivityResultLauncher<kotlin.Unit> registerForActivityResult(androidx.activity.result.ActivityResultCaller, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, kotlin.jvm.functions.Function1<? super O,kotlin.Unit> callback);
+  }
+
+  public final class ActivityResultKt {
+    method public static operator int component1(androidx.activity.result.ActivityResult);
+    method public static operator android.content.Intent? component2(androidx.activity.result.ActivityResult);
+  }
+
+  public final class ActivityResultLauncherKt {
+    method public static void launch(androidx.activity.result.ActivityResultLauncher<java.lang.Void>, optional androidx.core.app.ActivityOptionsCompat? options);
+    method public static void launchUnit(androidx.activity.result.ActivityResultLauncher<kotlin.Unit>, optional androidx.core.app.ActivityOptionsCompat? options);
+  }
+
+}
+
diff --git a/activity/activity-ktx/api/res-1.8.0-beta01.txt b/activity/activity-ktx/api/res-1.8.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/activity/activity-ktx/api/res-1.8.0-beta01.txt
diff --git a/activity/activity-ktx/api/restricted_1.8.0-beta01.txt b/activity/activity-ktx/api/restricted_1.8.0-beta01.txt
new file mode 100644
index 0000000..7b96bf7
--- /dev/null
+++ b/activity/activity-ktx/api/restricted_1.8.0-beta01.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.activity {
+
+  public final class ActivityViewModelLazyKt {
+    method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+    method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+  }
+
+  public final class PipHintTrackerKt {
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public static suspend Object? trackPipAnimationHintView(android.app.Activity, android.view.View view, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  }
+
+}
+
+package androidx.activity.result {
+
+  public final class ActivityResultCallerKt {
+    method public static <I, O> androidx.activity.result.ActivityResultLauncher<kotlin.Unit> registerForActivityResult(androidx.activity.result.ActivityResultCaller, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, androidx.activity.result.ActivityResultRegistry registry, kotlin.jvm.functions.Function1<? super O,kotlin.Unit> callback);
+    method public static <I, O> androidx.activity.result.ActivityResultLauncher<kotlin.Unit> registerForActivityResult(androidx.activity.result.ActivityResultCaller, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, kotlin.jvm.functions.Function1<? super O,kotlin.Unit> callback);
+  }
+
+  public final class ActivityResultKt {
+    method public static operator int component1(androidx.activity.result.ActivityResult);
+    method public static operator android.content.Intent? component2(androidx.activity.result.ActivityResult);
+  }
+
+  public final class ActivityResultLauncherKt {
+    method public static void launch(androidx.activity.result.ActivityResultLauncher<java.lang.Void>, optional androidx.core.app.ActivityOptionsCompat? options);
+    method public static void launchUnit(androidx.activity.result.ActivityResultLauncher<kotlin.Unit>, optional androidx.core.app.ActivityOptionsCompat? options);
+  }
+
+}
+
diff --git a/activity/activity/api/1.8.0-beta01.txt b/activity/activity/api/1.8.0-beta01.txt
new file mode 100644
index 0000000..f55a193
--- /dev/null
+++ b/activity/activity/api/1.8.0-beta01.txt
@@ -0,0 +1,469 @@
+// Signature format: 4.0
+package androidx.activity {
+
+  public final class BackEventCompat {
+    ctor @RequiresApi(34) public BackEventCompat(android.window.BackEvent backEvent);
+    ctor @VisibleForTesting public BackEventCompat(float touchX, float touchY, @FloatRange(from=0.0, to=1.0) float progress, int swipeEdge);
+    method public float getProgress();
+    method public int getSwipeEdge();
+    method public float getTouchX();
+    method public float getTouchY();
+    method @RequiresApi(34) public android.window.BackEvent toBackEvent();
+    property public final float progress;
+    property public final int swipeEdge;
+    property public final float touchX;
+    property public final float touchY;
+    field public static final androidx.activity.BackEventCompat.Companion Companion;
+    field public static final int EDGE_LEFT = 0; // 0x0
+    field public static final int EDGE_RIGHT = 1; // 0x1
+  }
+
+  public static final class BackEventCompat.Companion {
+  }
+
+  public class ComponentActivity extends android.app.Activity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.activity.FullyDrawnReporterOwner androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.app.OnMultiWindowModeChangedProvider androidx.core.app.OnNewIntentProvider androidx.core.app.OnPictureInPictureModeChangedProvider androidx.core.content.OnTrimMemoryProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
+    ctor public ComponentActivity();
+    ctor @ContentView public ComponentActivity(@LayoutRes int);
+    method public void addMenuProvider(androidx.core.view.MenuProvider);
+    method public void addMenuProvider(androidx.core.view.MenuProvider, androidx.lifecycle.LifecycleOwner);
+    method public void addMenuProvider(androidx.core.view.MenuProvider, androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State);
+    method public final void addOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
+    method public final void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void addOnMultiWindowModeChangedListener(androidx.core.util.Consumer<androidx.core.app.MultiWindowModeChangedInfo!>);
+    method public final void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+    method public final void addOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo!>);
+    method public final void addOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
+    method public final androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
+    method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+    method public androidx.activity.FullyDrawnReporter getFullyDrawnReporter();
+    method @Deprecated public Object? getLastCustomNonConfigurationInstance();
+    method public androidx.lifecycle.Lifecycle getLifecycle();
+    method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    method public final androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
+    method public androidx.lifecycle.ViewModelStore getViewModelStore();
+    method @CallSuper public void initializeViewTreeOwners();
+    method public void invalidateMenu();
+    method @Deprecated @CallSuper protected void onActivityResult(int, int, android.content.Intent?);
+    method @CallSuper public void onMultiWindowModeChanged(boolean);
+    method @CallSuper public void onPictureInPictureModeChanged(boolean);
+    method @Deprecated @CallSuper public void onRequestPermissionsResult(int, String![], int[]);
+    method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
+    method public final Object? onRetainNonConfigurationInstance();
+    method public android.content.Context? peekAvailableContext();
+    method public final <I, O> androidx.activity.result.ActivityResultLauncher<I!> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultCallback<O!>);
+    method public final <I, O> androidx.activity.result.ActivityResultLauncher<I!> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultRegistry, androidx.activity.result.ActivityResultCallback<O!>);
+    method public void removeMenuProvider(androidx.core.view.MenuProvider);
+    method public final void removeOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
+    method public final void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void removeOnMultiWindowModeChangedListener(androidx.core.util.Consumer<androidx.core.app.MultiWindowModeChangedInfo!>);
+    method public final void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+    method public final void removeOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo!>);
+    method public final void removeOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent?, int, int, int) throws android.content.IntentSender.SendIntentException;
+    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+  }
+
+  public class ComponentDialog extends android.app.Dialog implements androidx.lifecycle.LifecycleOwner androidx.activity.OnBackPressedDispatcherOwner androidx.savedstate.SavedStateRegistryOwner {
+    ctor public ComponentDialog(android.content.Context context);
+    ctor public ComponentDialog(android.content.Context context, optional @StyleRes int themeResId);
+    method public androidx.lifecycle.Lifecycle getLifecycle();
+    method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
+    method @CallSuper public void initializeViewTreeOwners();
+    method @CallSuper public void onBackPressed();
+    property public androidx.lifecycle.Lifecycle lifecycle;
+    property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
+    property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+  }
+
+  public final class EdgeToEdge {
+    method public static void enable(androidx.activity.ComponentActivity);
+    method public static void enable(androidx.activity.ComponentActivity, optional androidx.activity.SystemBarStyle statusBarStyle);
+    method public static void enable(androidx.activity.ComponentActivity, optional androidx.activity.SystemBarStyle statusBarStyle, optional androidx.activity.SystemBarStyle navigationBarStyle);
+  }
+
+  public final class FullyDrawnReporter {
+    ctor public FullyDrawnReporter(java.util.concurrent.Executor executor, kotlin.jvm.functions.Function0<kotlin.Unit> reportFullyDrawn);
+    method public void addOnReportDrawnListener(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
+    method public void addReporter();
+    method public boolean isFullyDrawnReported();
+    method public void removeOnReportDrawnListener(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
+    method public void removeReporter();
+    property public final boolean isFullyDrawnReported;
+  }
+
+  public final class FullyDrawnReporterKt {
+    method public static suspend inline Object? reportWhenComplete(androidx.activity.FullyDrawnReporter, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> reporter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  }
+
+  public interface FullyDrawnReporterOwner {
+    method public androidx.activity.FullyDrawnReporter getFullyDrawnReporter();
+    property public abstract androidx.activity.FullyDrawnReporter fullyDrawnReporter;
+  }
+
+  public abstract class OnBackPressedCallback {
+    ctor public OnBackPressedCallback(boolean enabled);
+    method @MainThread public void handleOnBackCancelled();
+    method @MainThread public abstract void handleOnBackPressed();
+    method @MainThread public void handleOnBackProgressed(androidx.activity.BackEventCompat backEvent);
+    method @MainThread public void handleOnBackStarted(androidx.activity.BackEventCompat backEvent);
+    method @MainThread public final boolean isEnabled();
+    method @MainThread public final void remove();
+    method @MainThread public final void setEnabled(boolean);
+    property @MainThread public final boolean isEnabled;
+  }
+
+  public final class OnBackPressedDispatcher {
+    ctor public OnBackPressedDispatcher();
+    ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
+    ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
+    method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
+    method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
+    method @MainThread @VisibleForTesting public void dispatchOnBackCancelled();
+    method @MainThread @VisibleForTesting public void dispatchOnBackProgressed(androidx.activity.BackEventCompat backEvent);
+    method @MainThread @VisibleForTesting public void dispatchOnBackStarted(androidx.activity.BackEventCompat backEvent);
+    method @MainThread public boolean hasEnabledCallbacks();
+    method @MainThread public void onBackPressed();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
+  }
+
+  public final class OnBackPressedDispatcherKt {
+    method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
+  }
+
+  public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
+    method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    property public abstract androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
+  }
+
+  public final class SystemBarStyle {
+    method public static androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim);
+    method public static androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim, optional kotlin.jvm.functions.Function1<? super android.content.res.Resources,java.lang.Boolean> detectDarkMode);
+    method public static androidx.activity.SystemBarStyle dark(@ColorInt int scrim);
+    method public static androidx.activity.SystemBarStyle light(@ColorInt int scrim, @ColorInt int darkScrim);
+    field public static final androidx.activity.SystemBarStyle.Companion Companion;
+  }
+
+  public static final class SystemBarStyle.Companion {
+    method public androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim);
+    method public androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim, optional kotlin.jvm.functions.Function1<? super android.content.res.Resources,java.lang.Boolean> detectDarkMode);
+    method public androidx.activity.SystemBarStyle dark(@ColorInt int scrim);
+    method public androidx.activity.SystemBarStyle light(@ColorInt int scrim, @ColorInt int darkScrim);
+  }
+
+  public final class ViewTreeFullyDrawnReporterOwner {
+    method public static androidx.activity.FullyDrawnReporterOwner? get(android.view.View);
+    method public static void set(android.view.View, androidx.activity.FullyDrawnReporterOwner fullyDrawnReporterOwner);
+  }
+
+  public final class ViewTreeOnBackPressedDispatcherOwner {
+    method public static androidx.activity.OnBackPressedDispatcherOwner? get(android.view.View);
+    method public static void set(android.view.View, androidx.activity.OnBackPressedDispatcherOwner onBackPressedDispatcherOwner);
+  }
+
+}
+
+package androidx.activity.contextaware {
+
+  public interface ContextAware {
+    method public void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
+    method public android.content.Context? peekAvailableContext();
+    method public void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
+  }
+
+  public final class ContextAwareHelper {
+    ctor public ContextAwareHelper();
+    method public void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
+    method public void clearAvailableContext();
+    method public void dispatchOnContextAvailable(android.content.Context context);
+    method public android.content.Context? peekAvailableContext();
+    method public void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
+  }
+
+  public final class ContextAwareKt {
+    method public static suspend inline <R> Object? withContextAvailable(androidx.activity.contextaware.ContextAware, kotlin.jvm.functions.Function1<android.content.Context,R> onContextAvailable, kotlin.coroutines.Continuation<R>);
+  }
+
+  public fun interface OnContextAvailableListener {
+    method public void onContextAvailable(android.content.Context context);
+  }
+
+}
+
+package androidx.activity.result {
+
+  public final class ActivityResult implements android.os.Parcelable {
+    ctor public ActivityResult(int, android.content.Intent?);
+    method public int describeContents();
+    method public android.content.Intent? getData();
+    method public int getResultCode();
+    method public static String resultCodeToString(int);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<androidx.activity.result.ActivityResult!> CREATOR;
+  }
+
+  public fun interface ActivityResultCallback<O> {
+    method public void onActivityResult(O result);
+  }
+
+  public interface ActivityResultCaller {
+    method public <I, O> androidx.activity.result.ActivityResultLauncher<I!> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultCallback<O!>);
+    method public <I, O> androidx.activity.result.ActivityResultLauncher<I!> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultRegistry, androidx.activity.result.ActivityResultCallback<O!>);
+  }
+
+  public abstract class ActivityResultLauncher<I> {
+    ctor public ActivityResultLauncher();
+    method public abstract androidx.activity.result.contract.ActivityResultContract<I!,?> getContract();
+    method public void launch(I!);
+    method public abstract void launch(I!, androidx.core.app.ActivityOptionsCompat?);
+    method @MainThread public abstract void unregister();
+  }
+
+  public abstract class ActivityResultRegistry {
+    ctor public ActivityResultRegistry();
+    method @MainThread public final boolean dispatchResult(int, int, android.content.Intent?);
+    method @MainThread public final <O> boolean dispatchResult(int, O!);
+    method @MainThread public abstract <I, O> void onLaunch(int, androidx.activity.result.contract.ActivityResultContract<I!,O!>, I!, androidx.core.app.ActivityOptionsCompat?);
+    method public final void onRestoreInstanceState(android.os.Bundle?);
+    method public final void onSaveInstanceState(android.os.Bundle);
+    method public final <I, O> androidx.activity.result.ActivityResultLauncher<I!> register(String, androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultCallback<O!>);
+    method public final <I, O> androidx.activity.result.ActivityResultLauncher<I!> register(String, androidx.lifecycle.LifecycleOwner, androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultCallback<O!>);
+  }
+
+  public interface ActivityResultRegistryOwner {
+    method public androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
+    property public abstract androidx.activity.result.ActivityResultRegistry activityResultRegistry;
+  }
+
+  public final class IntentSenderRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method public android.content.Intent? getFillInIntent();
+    method public int getFlagsMask();
+    method public int getFlagsValues();
+    method public android.content.IntentSender getIntentSender();
+    method public void writeToParcel(android.os.Parcel dest, int flags);
+    property public final android.content.Intent? fillInIntent;
+    property public final int flagsMask;
+    property public final int flagsValues;
+    property public final android.content.IntentSender intentSender;
+    field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest> CREATOR;
+    field public static final androidx.activity.result.IntentSenderRequest.Companion Companion;
+  }
+
+  public static final class IntentSenderRequest.Builder {
+    ctor public IntentSenderRequest.Builder(android.app.PendingIntent pendingIntent);
+    ctor public IntentSenderRequest.Builder(android.content.IntentSender intentSender);
+    method public androidx.activity.result.IntentSenderRequest build();
+    method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent? fillInIntent);
+    method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int values, int mask);
+  }
+
+  public static final class IntentSenderRequest.Companion {
+  }
+
+  public final class PickVisualMediaRequest {
+    method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+    property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
+  }
+
+  public static final class PickVisualMediaRequest.Builder {
+    ctor public PickVisualMediaRequest.Builder();
+    method public androidx.activity.result.PickVisualMediaRequest build();
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
+  public final class PickVisualMediaRequestKt {
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
+}
+
+package androidx.activity.result.contract {
+
+  public abstract class ActivityResultContract<I, O> {
+    ctor public ActivityResultContract();
+    method public abstract android.content.Intent createIntent(android.content.Context context, I input);
+    method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<O>? getSynchronousResult(android.content.Context context, I input);
+    method public abstract O parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public static final class ActivityResultContract.SynchronousResult<T> {
+    ctor public ActivityResultContract.SynchronousResult(T value);
+    method public T getValue();
+    property public final T value;
+  }
+
+  public final class ActivityResultContracts {
+  }
+
+  public static class ActivityResultContracts.CaptureVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,java.lang.Boolean> {
+    ctor public ActivityResultContracts.CaptureVideo();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, android.net.Uri input);
+    method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(19) 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);
+    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);
+  }
+
+  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);
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(18) public static class ActivityResultContracts.GetMultipleContents extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.GetMultipleContents();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, String input);
+    method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(19) 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> {
+    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);
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(19) public static class ActivityResultContracts.OpenMultipleDocuments extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.OpenMultipleDocuments();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, String![] input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, String![] input);
+    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> {
+    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);
+  }
+
+  @RequiresApi(19) public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(19) public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+    ctor public ActivityResultContracts.PickVisualMedia();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method @Deprecated public static final boolean isPhotoPickerAvailable();
+    method public static final boolean isPhotoPickerAvailable(android.content.Context context);
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+    field public static final String ACTION_SYSTEM_FALLBACK_PICK_IMAGES = "androidx.activity.result.contract.action.PICK_IMAGES";
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+    field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX = "androidx.activity.result.contract.extra.PICK_IMAGES_MAX";
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.Companion {
+    method @Deprecated public boolean isPhotoPickerAvailable();
+    method public boolean isPhotoPickerAvailable(android.content.Context context);
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
+    method public String getMimeType();
+    property public final String mimeType;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
+  }
+
+  public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
+  }
+
+  public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
+    ctor public ActivityResultContracts.RequestMultiplePermissions();
+    method public android.content.Intent createIntent(android.content.Context context, String![] input);
+    method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.Map<java.lang.String,java.lang.Boolean>>? getSynchronousResult(android.content.Context context, String![] input);
+    method public java.util.Map<java.lang.String,java.lang.Boolean> parseResult(int resultCode, android.content.Intent? intent);
+    field public static final String ACTION_REQUEST_PERMISSIONS = "androidx.activity.result.contract.action.REQUEST_PERMISSIONS";
+    field public static final androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions.Companion Companion;
+    field public static final String EXTRA_PERMISSIONS = "androidx.activity.result.contract.extra.PERMISSIONS";
+    field public static final String EXTRA_PERMISSION_GRANT_RESULTS = "androidx.activity.result.contract.extra.PERMISSION_GRANT_RESULTS";
+  }
+
+  public static final class ActivityResultContracts.RequestMultiplePermissions.Companion {
+  }
+
+  public static final class ActivityResultContracts.RequestPermission extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,java.lang.Boolean> {
+    ctor public ActivityResultContracts.RequestPermission();
+    method public android.content.Intent createIntent(android.content.Context context, String input);
+    method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, String input);
+    method public Boolean parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public static final class ActivityResultContracts.StartActivityForResult extends androidx.activity.result.contract.ActivityResultContract<android.content.Intent,androidx.activity.result.ActivityResult> {
+    ctor public ActivityResultContracts.StartActivityForResult();
+    method public android.content.Intent createIntent(android.content.Context context, android.content.Intent input);
+    method public androidx.activity.result.ActivityResult parseResult(int resultCode, android.content.Intent? intent);
+    field public static final androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult.Companion Companion;
+    field public static final String EXTRA_ACTIVITY_OPTIONS_BUNDLE = "androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE";
+  }
+
+  public static final class ActivityResultContracts.StartActivityForResult.Companion {
+  }
+
+  public static final class ActivityResultContracts.StartIntentSenderForResult extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.IntentSenderRequest,androidx.activity.result.ActivityResult> {
+    ctor public ActivityResultContracts.StartIntentSenderForResult();
+    method public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.IntentSenderRequest input);
+    method public androidx.activity.result.ActivityResult parseResult(int resultCode, android.content.Intent? intent);
+    field public static final String ACTION_INTENT_SENDER_REQUEST = "androidx.activity.result.contract.action.INTENT_SENDER_REQUEST";
+    field public static final androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.Companion Companion;
+    field public static final String EXTRA_INTENT_SENDER_REQUEST = "androidx.activity.result.contract.extra.INTENT_SENDER_REQUEST";
+    field public static final String EXTRA_SEND_INTENT_EXCEPTION = "androidx.activity.result.contract.extra.SEND_INTENT_EXCEPTION";
+  }
+
+  public static final class ActivityResultContracts.StartIntentSenderForResult.Companion {
+  }
+
+  public static class ActivityResultContracts.TakePicture extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,java.lang.Boolean> {
+    ctor public ActivityResultContracts.TakePicture();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, android.net.Uri input);
+    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> {
+    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> {
+    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);
+    method @Deprecated public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+}
+
diff --git a/activity/activity/api/res-1.8.0-beta01.txt b/activity/activity/api/res-1.8.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/activity/activity/api/res-1.8.0-beta01.txt
diff --git a/activity/activity/api/restricted_1.8.0-beta01.txt b/activity/activity/api/restricted_1.8.0-beta01.txt
new file mode 100644
index 0000000..0348b24
--- /dev/null
+++ b/activity/activity/api/restricted_1.8.0-beta01.txt
@@ -0,0 +1,468 @@
+// Signature format: 4.0
+package androidx.activity {
+
+  public final class BackEventCompat {
+    ctor @RequiresApi(34) public BackEventCompat(android.window.BackEvent backEvent);
+    ctor @VisibleForTesting public BackEventCompat(float touchX, float touchY, @FloatRange(from=0.0, to=1.0) float progress, int swipeEdge);
+    method public float getProgress();
+    method public int getSwipeEdge();
+    method public float getTouchX();
+    method public float getTouchY();
+    method @RequiresApi(34) public android.window.BackEvent toBackEvent();
+    property public final float progress;
+    property public final int swipeEdge;
+    property public final float touchX;
+    property public final float touchY;
+    field public static final androidx.activity.BackEventCompat.Companion Companion;
+    field public static final int EDGE_LEFT = 0; // 0x0
+    field public static final int EDGE_RIGHT = 1; // 0x1
+  }
+
+  public static final class BackEventCompat.Companion {
+  }
+
+  public class ComponentActivity extends androidx.core.app.ComponentActivity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.activity.FullyDrawnReporterOwner androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.app.OnMultiWindowModeChangedProvider androidx.core.app.OnNewIntentProvider androidx.core.app.OnPictureInPictureModeChangedProvider androidx.core.content.OnTrimMemoryProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
+    ctor public ComponentActivity();
+    ctor @ContentView public ComponentActivity(@LayoutRes int);
+    method public void addMenuProvider(androidx.core.view.MenuProvider);
+    method public void addMenuProvider(androidx.core.view.MenuProvider, androidx.lifecycle.LifecycleOwner);
+    method public void addMenuProvider(androidx.core.view.MenuProvider, androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State);
+    method public final void addOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
+    method public final void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void addOnMultiWindowModeChangedListener(androidx.core.util.Consumer<androidx.core.app.MultiWindowModeChangedInfo!>);
+    method public final void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+    method public final void addOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo!>);
+    method public final void addOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
+    method public final androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
+    method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+    method public androidx.activity.FullyDrawnReporter getFullyDrawnReporter();
+    method @Deprecated public Object? getLastCustomNonConfigurationInstance();
+    method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    method public final androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
+    method public androidx.lifecycle.ViewModelStore getViewModelStore();
+    method @CallSuper public void initializeViewTreeOwners();
+    method public void invalidateMenu();
+    method @Deprecated @CallSuper protected void onActivityResult(int, int, android.content.Intent?);
+    method @CallSuper public void onMultiWindowModeChanged(boolean);
+    method @CallSuper public void onPictureInPictureModeChanged(boolean);
+    method @Deprecated @CallSuper public void onRequestPermissionsResult(int, String![], int[]);
+    method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
+    method public final Object? onRetainNonConfigurationInstance();
+    method public android.content.Context? peekAvailableContext();
+    method public final <I, O> androidx.activity.result.ActivityResultLauncher<I!> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultCallback<O!>);
+    method public final <I, O> androidx.activity.result.ActivityResultLauncher<I!> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultRegistry, androidx.activity.result.ActivityResultCallback<O!>);
+    method public void removeMenuProvider(androidx.core.view.MenuProvider);
+    method public final void removeOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
+    method public final void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void removeOnMultiWindowModeChangedListener(androidx.core.util.Consumer<androidx.core.app.MultiWindowModeChangedInfo!>);
+    method public final void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+    method public final void removeOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo!>);
+    method public final void removeOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent?, int, int, int) throws android.content.IntentSender.SendIntentException;
+    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+  }
+
+  public class ComponentDialog extends android.app.Dialog implements androidx.lifecycle.LifecycleOwner androidx.activity.OnBackPressedDispatcherOwner androidx.savedstate.SavedStateRegistryOwner {
+    ctor public ComponentDialog(android.content.Context context);
+    ctor public ComponentDialog(android.content.Context context, optional @StyleRes int themeResId);
+    method public androidx.lifecycle.Lifecycle getLifecycle();
+    method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
+    method @CallSuper public void initializeViewTreeOwners();
+    method @CallSuper public void onBackPressed();
+    property public androidx.lifecycle.Lifecycle lifecycle;
+    property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
+    property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+  }
+
+  public final class EdgeToEdge {
+    method public static void enable(androidx.activity.ComponentActivity);
+    method public static void enable(androidx.activity.ComponentActivity, optional androidx.activity.SystemBarStyle statusBarStyle);
+    method public static void enable(androidx.activity.ComponentActivity, optional androidx.activity.SystemBarStyle statusBarStyle, optional androidx.activity.SystemBarStyle navigationBarStyle);
+  }
+
+  public final class FullyDrawnReporter {
+    ctor public FullyDrawnReporter(java.util.concurrent.Executor executor, kotlin.jvm.functions.Function0<kotlin.Unit> reportFullyDrawn);
+    method public void addOnReportDrawnListener(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
+    method public void addReporter();
+    method public boolean isFullyDrawnReported();
+    method public void removeOnReportDrawnListener(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
+    method public void removeReporter();
+    property public final boolean isFullyDrawnReported;
+  }
+
+  public final class FullyDrawnReporterKt {
+    method public static suspend inline Object? reportWhenComplete(androidx.activity.FullyDrawnReporter, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> reporter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  }
+
+  public interface FullyDrawnReporterOwner {
+    method public androidx.activity.FullyDrawnReporter getFullyDrawnReporter();
+    property public abstract androidx.activity.FullyDrawnReporter fullyDrawnReporter;
+  }
+
+  public abstract class OnBackPressedCallback {
+    ctor public OnBackPressedCallback(boolean enabled);
+    method @MainThread public void handleOnBackCancelled();
+    method @MainThread public abstract void handleOnBackPressed();
+    method @MainThread public void handleOnBackProgressed(androidx.activity.BackEventCompat backEvent);
+    method @MainThread public void handleOnBackStarted(androidx.activity.BackEventCompat backEvent);
+    method @MainThread public final boolean isEnabled();
+    method @MainThread public final void remove();
+    method @MainThread public final void setEnabled(boolean);
+    property @MainThread public final boolean isEnabled;
+  }
+
+  public final class OnBackPressedDispatcher {
+    ctor public OnBackPressedDispatcher();
+    ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
+    ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
+    method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
+    method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
+    method @MainThread @VisibleForTesting public void dispatchOnBackCancelled();
+    method @MainThread @VisibleForTesting public void dispatchOnBackProgressed(androidx.activity.BackEventCompat backEvent);
+    method @MainThread @VisibleForTesting public void dispatchOnBackStarted(androidx.activity.BackEventCompat backEvent);
+    method @MainThread public boolean hasEnabledCallbacks();
+    method @MainThread public void onBackPressed();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
+  }
+
+  public final class OnBackPressedDispatcherKt {
+    method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
+  }
+
+  public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
+    method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    property public abstract androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
+  }
+
+  public final class SystemBarStyle {
+    method public static androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim);
+    method public static androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim, optional kotlin.jvm.functions.Function1<? super android.content.res.Resources,java.lang.Boolean> detectDarkMode);
+    method public static androidx.activity.SystemBarStyle dark(@ColorInt int scrim);
+    method public static androidx.activity.SystemBarStyle light(@ColorInt int scrim, @ColorInt int darkScrim);
+    field public static final androidx.activity.SystemBarStyle.Companion Companion;
+  }
+
+  public static final class SystemBarStyle.Companion {
+    method public androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim);
+    method public androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim, optional kotlin.jvm.functions.Function1<? super android.content.res.Resources,java.lang.Boolean> detectDarkMode);
+    method public androidx.activity.SystemBarStyle dark(@ColorInt int scrim);
+    method public androidx.activity.SystemBarStyle light(@ColorInt int scrim, @ColorInt int darkScrim);
+  }
+
+  public final class ViewTreeFullyDrawnReporterOwner {
+    method public static androidx.activity.FullyDrawnReporterOwner? get(android.view.View);
+    method public static void set(android.view.View, androidx.activity.FullyDrawnReporterOwner fullyDrawnReporterOwner);
+  }
+
+  public final class ViewTreeOnBackPressedDispatcherOwner {
+    method public static androidx.activity.OnBackPressedDispatcherOwner? get(android.view.View);
+    method public static void set(android.view.View, androidx.activity.OnBackPressedDispatcherOwner onBackPressedDispatcherOwner);
+  }
+
+}
+
+package androidx.activity.contextaware {
+
+  public interface ContextAware {
+    method public void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
+    method public android.content.Context? peekAvailableContext();
+    method public void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
+  }
+
+  public final class ContextAwareHelper {
+    ctor public ContextAwareHelper();
+    method public void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
+    method public void clearAvailableContext();
+    method public void dispatchOnContextAvailable(android.content.Context context);
+    method public android.content.Context? peekAvailableContext();
+    method public void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
+  }
+
+  public final class ContextAwareKt {
+    method public static suspend inline <R> Object? withContextAvailable(androidx.activity.contextaware.ContextAware, kotlin.jvm.functions.Function1<android.content.Context,R> onContextAvailable, kotlin.coroutines.Continuation<R>);
+  }
+
+  public fun interface OnContextAvailableListener {
+    method public void onContextAvailable(android.content.Context context);
+  }
+
+}
+
+package androidx.activity.result {
+
+  public final class ActivityResult implements android.os.Parcelable {
+    ctor public ActivityResult(int, android.content.Intent?);
+    method public int describeContents();
+    method public android.content.Intent? getData();
+    method public int getResultCode();
+    method public static String resultCodeToString(int);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<androidx.activity.result.ActivityResult!> CREATOR;
+  }
+
+  public fun interface ActivityResultCallback<O> {
+    method public void onActivityResult(O result);
+  }
+
+  public interface ActivityResultCaller {
+    method public <I, O> androidx.activity.result.ActivityResultLauncher<I!> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultCallback<O!>);
+    method public <I, O> androidx.activity.result.ActivityResultLauncher<I!> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultRegistry, androidx.activity.result.ActivityResultCallback<O!>);
+  }
+
+  public abstract class ActivityResultLauncher<I> {
+    ctor public ActivityResultLauncher();
+    method public abstract androidx.activity.result.contract.ActivityResultContract<I!,?> getContract();
+    method public void launch(I!);
+    method public abstract void launch(I!, androidx.core.app.ActivityOptionsCompat?);
+    method @MainThread public abstract void unregister();
+  }
+
+  public abstract class ActivityResultRegistry {
+    ctor public ActivityResultRegistry();
+    method @MainThread public final boolean dispatchResult(int, int, android.content.Intent?);
+    method @MainThread public final <O> boolean dispatchResult(int, O!);
+    method @MainThread public abstract <I, O> void onLaunch(int, androidx.activity.result.contract.ActivityResultContract<I!,O!>, I!, androidx.core.app.ActivityOptionsCompat?);
+    method public final void onRestoreInstanceState(android.os.Bundle?);
+    method public final void onSaveInstanceState(android.os.Bundle);
+    method public final <I, O> androidx.activity.result.ActivityResultLauncher<I!> register(String, androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultCallback<O!>);
+    method public final <I, O> androidx.activity.result.ActivityResultLauncher<I!> register(String, androidx.lifecycle.LifecycleOwner, androidx.activity.result.contract.ActivityResultContract<I!,O!>, androidx.activity.result.ActivityResultCallback<O!>);
+  }
+
+  public interface ActivityResultRegistryOwner {
+    method public androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
+    property public abstract androidx.activity.result.ActivityResultRegistry activityResultRegistry;
+  }
+
+  public final class IntentSenderRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method public android.content.Intent? getFillInIntent();
+    method public int getFlagsMask();
+    method public int getFlagsValues();
+    method public android.content.IntentSender getIntentSender();
+    method public void writeToParcel(android.os.Parcel dest, int flags);
+    property public final android.content.Intent? fillInIntent;
+    property public final int flagsMask;
+    property public final int flagsValues;
+    property public final android.content.IntentSender intentSender;
+    field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest> CREATOR;
+    field public static final androidx.activity.result.IntentSenderRequest.Companion Companion;
+  }
+
+  public static final class IntentSenderRequest.Builder {
+    ctor public IntentSenderRequest.Builder(android.app.PendingIntent pendingIntent);
+    ctor public IntentSenderRequest.Builder(android.content.IntentSender intentSender);
+    method public androidx.activity.result.IntentSenderRequest build();
+    method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent? fillInIntent);
+    method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int values, int mask);
+  }
+
+  public static final class IntentSenderRequest.Companion {
+  }
+
+  public final class PickVisualMediaRequest {
+    method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+    property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
+  }
+
+  public static final class PickVisualMediaRequest.Builder {
+    ctor public PickVisualMediaRequest.Builder();
+    method public androidx.activity.result.PickVisualMediaRequest build();
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
+  public final class PickVisualMediaRequestKt {
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
+}
+
+package androidx.activity.result.contract {
+
+  public abstract class ActivityResultContract<I, O> {
+    ctor public ActivityResultContract();
+    method public abstract android.content.Intent createIntent(android.content.Context context, I input);
+    method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<O>? getSynchronousResult(android.content.Context context, I input);
+    method public abstract O parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public static final class ActivityResultContract.SynchronousResult<T> {
+    ctor public ActivityResultContract.SynchronousResult(T value);
+    method public T getValue();
+    property public final T value;
+  }
+
+  public final class ActivityResultContracts {
+  }
+
+  public static class ActivityResultContracts.CaptureVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,java.lang.Boolean> {
+    ctor public ActivityResultContracts.CaptureVideo();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, android.net.Uri input);
+    method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(19) 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);
+    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);
+  }
+
+  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);
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(18) public static class ActivityResultContracts.GetMultipleContents extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.GetMultipleContents();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, String input);
+    method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(19) 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> {
+    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);
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(19) public static class ActivityResultContracts.OpenMultipleDocuments extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.OpenMultipleDocuments();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, String![] input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, String![] input);
+    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> {
+    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);
+  }
+
+  @RequiresApi(19) public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  @RequiresApi(19) public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+    ctor public ActivityResultContracts.PickVisualMedia();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method @Deprecated public static final boolean isPhotoPickerAvailable();
+    method public static final boolean isPhotoPickerAvailable(android.content.Context context);
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+    field public static final String ACTION_SYSTEM_FALLBACK_PICK_IMAGES = "androidx.activity.result.contract.action.PICK_IMAGES";
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+    field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX = "androidx.activity.result.contract.extra.PICK_IMAGES_MAX";
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.Companion {
+    method @Deprecated public boolean isPhotoPickerAvailable();
+    method public boolean isPhotoPickerAvailable(android.content.Context context);
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
+    method public String getMimeType();
+    property public final String mimeType;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
+  }
+
+  public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
+  }
+
+  public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
+    ctor public ActivityResultContracts.RequestMultiplePermissions();
+    method public android.content.Intent createIntent(android.content.Context context, String![] input);
+    method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.Map<java.lang.String,java.lang.Boolean>>? getSynchronousResult(android.content.Context context, String![] input);
+    method public java.util.Map<java.lang.String,java.lang.Boolean> parseResult(int resultCode, android.content.Intent? intent);
+    field public static final String ACTION_REQUEST_PERMISSIONS = "androidx.activity.result.contract.action.REQUEST_PERMISSIONS";
+    field public static final androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions.Companion Companion;
+    field public static final String EXTRA_PERMISSIONS = "androidx.activity.result.contract.extra.PERMISSIONS";
+    field public static final String EXTRA_PERMISSION_GRANT_RESULTS = "androidx.activity.result.contract.extra.PERMISSION_GRANT_RESULTS";
+  }
+
+  public static final class ActivityResultContracts.RequestMultiplePermissions.Companion {
+  }
+
+  public static final class ActivityResultContracts.RequestPermission extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,java.lang.Boolean> {
+    ctor public ActivityResultContracts.RequestPermission();
+    method public android.content.Intent createIntent(android.content.Context context, String input);
+    method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, String input);
+    method public Boolean parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public static final class ActivityResultContracts.StartActivityForResult extends androidx.activity.result.contract.ActivityResultContract<android.content.Intent,androidx.activity.result.ActivityResult> {
+    ctor public ActivityResultContracts.StartActivityForResult();
+    method public android.content.Intent createIntent(android.content.Context context, android.content.Intent input);
+    method public androidx.activity.result.ActivityResult parseResult(int resultCode, android.content.Intent? intent);
+    field public static final androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult.Companion Companion;
+    field public static final String EXTRA_ACTIVITY_OPTIONS_BUNDLE = "androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE";
+  }
+
+  public static final class ActivityResultContracts.StartActivityForResult.Companion {
+  }
+
+  public static final class ActivityResultContracts.StartIntentSenderForResult extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.IntentSenderRequest,androidx.activity.result.ActivityResult> {
+    ctor public ActivityResultContracts.StartIntentSenderForResult();
+    method public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.IntentSenderRequest input);
+    method public androidx.activity.result.ActivityResult parseResult(int resultCode, android.content.Intent? intent);
+    field public static final String ACTION_INTENT_SENDER_REQUEST = "androidx.activity.result.contract.action.INTENT_SENDER_REQUEST";
+    field public static final androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.Companion Companion;
+    field public static final String EXTRA_INTENT_SENDER_REQUEST = "androidx.activity.result.contract.extra.INTENT_SENDER_REQUEST";
+    field public static final String EXTRA_SEND_INTENT_EXCEPTION = "androidx.activity.result.contract.extra.SEND_INTENT_EXCEPTION";
+  }
+
+  public static final class ActivityResultContracts.StartIntentSenderForResult.Companion {
+  }
+
+  public static class ActivityResultContracts.TakePicture extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,java.lang.Boolean> {
+    ctor public ActivityResultContracts.TakePicture();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, android.net.Uri input);
+    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> {
+    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> {
+    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);
+    method @Deprecated public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+}
+
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Thing.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Thing.java
index cbcef7c..a9bd034 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Thing.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Thing.java
@@ -300,10 +300,9 @@
         @NonNull
         public T setAlternateNames(@Nullable List<String> alternateNames) {
             resetIfBuilt();
+            clearAlternateNames();
             if (alternateNames != null) {
-                mAlternateNames = new ArrayList<>(alternateNames);
-            } else {
-                clearAlternateNames();
+                mAlternateNames.addAll(alternateNames);
             }
             return (T) this;
         }
@@ -367,10 +366,9 @@
         @NonNull
         public T setPotentialActions(@Nullable List<PotentialAction> newPotentialActions) {
             resetIfBuilt();
+            clearPotentialActions();
             if (newPotentialActions != null) {
-                mPotentialActions = new ArrayList<>(newPotentialActions);
-            } else {
-                clearPotentialActions();
+                mPotentialActions.addAll(newPotentialActions);
             }
             return (T) this;
         }
@@ -385,6 +383,9 @@
             return (T) this;
         }
 
+        /**
+         * If built, make a copy of previous data for every field so that the builder can be reused.
+         */
         private void resetIfBuilt() {
             if (mBuilt) {
                 mAlternateNames = new ArrayList<>(mAlternateNames);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
index 84661ff..4118c45 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -327,7 +327,6 @@
                 + "    {\n"
                 + "      name: \"document\",\n"
                 + "      shouldIndexNestedProperties: true,\n"
-                + "      indexableNestedProperties: [],\n"
                 + "      schemaType: \"builtin:Email\",\n"
                 + "      cardinality: CARDINALITY_REPEATED,\n"
                 + "      dataType: DATA_TYPE_DOCUMENT,\n"
@@ -408,7 +407,10 @@
                 + "  ]\n"
                 + "}";
 
-        assertThat(schemaString).isEqualTo(expectedString);
+        String[] lines = expectedString.split("\n");
+        for (String line : lines) {
+            assertThat(schemaString).contains(line);
+        }
     }
 
     @Test
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index 7bde81f..469d9aa 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -6704,4 +6704,42 @@
         assertThat(outDocuments).hasSize(1);
         assertThat(outDocuments).containsExactly(org2);
     }
+
+    @Test
+    public void testSetSchema_toString_containsIndexableNestedPropsList() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
+
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "sender", "Person")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(false)
+                                        .addIndexableNestedProperties(
+                                                Arrays.asList(
+                                                        "name", "worksFor.name", "worksFor.notes"))
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "recipient", "Person")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .build();
+        String expectedIndexableNestedPropertyMessage =
+                "indexableNestedProperties: [name, worksFor.notes, worksFor.name]";
+
+        assertThat(emailSchema.toString()).contains(expectedIndexableNestedPropertyMessage);
+
+    }
 }
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
index 846d743..3b3c6d2 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
@@ -61,12 +61,20 @@
     @Test
     public fun sanitizeFilename() {
         assertEquals(
-            "testFilename[one-Thing[],two-other]",
+            "testFilename_one_Thing_two_other_",
             Outputs.sanitizeFilename("testFilename[one=Thing( ),two:other]")
         )
     }
 
     @Test
+    public fun sanitizeFilename_withExtension() {
+        assertEquals(
+            "testFilename_one_Thing_two_other_.trace",
+            Outputs.sanitizeFilename("testFilename[one=Thing( ),two:other].trace")
+        )
+    }
+
+    @Test
     public fun testDateToFileName() {
         val date = Date(0)
         val expected = "1970-01-01-00-00-00"
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
index e612a1b..e609459 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
@@ -54,7 +54,7 @@
             // noop
         }
         assertNotNull(perfettoTrace)
-        assert(perfettoTrace!!.path.matches(Regex(".*/testTrace_[0-9-]+.perfetto-trace"))) {
+        assert(perfettoTrace!!.path.matches(Regex(".*/testTrace_[0-9_]+.perfetto-trace"))) {
             "$perfettoTrace didn't match!"
         }
     }
@@ -74,7 +74,7 @@
             // noop
         }
         assertNotNull(perfettoTrace)
-        assert(perfettoTrace!!.path.matches(Regex(".*/${label}_[0-9-]+.perfetto-trace"))) {
+        assert(perfettoTrace!!.path.matches(Regex(".*/${label}_[0-9_]+.perfetto-trace"))) {
             "$perfettoTrace didn't match!"
         }
     }
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
index dee870d..d3f120f 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
@@ -74,13 +74,13 @@
     @Test
     fun methodTracing() = verifyProfiler(
         profiler = MethodTracing,
-        regex = Regex("test-methodTracing-.+.trace")
+        regex = Regex("test_methodTracing_.+.trace")
     )
 
     @Test
     fun stackSamplingLegacy() = verifyProfiler(
         profiler = StackSamplingLegacy,
-        regex = Regex("test-stackSamplingLegacy-.+.trace")
+        regex = Regex("test_stackSamplingLegacy_.+.trace")
     )
 
     @SdkSuppress(minSdkVersion = 29) // simpleperf on system image starting API 29
@@ -91,7 +91,7 @@
 
         verifyProfiler(
             profiler = StackSamplingSimpleperf,
-            regex = Regex("test-stackSampling-.+.trace")
+            regex = Regex("test_stackSampling_.+.trace")
         )
     }
 }
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
index 310b07a..53c59e4 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
@@ -32,6 +32,8 @@
 
     private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
 
+    private val sanitizerRegex = Regex("(\\W+)")
+
     /**
      * The intended output directory that respects the `additionalTestOutputDir`.
      */
@@ -149,12 +151,15 @@
     }
 
     fun sanitizeFilename(filename: String): String {
-        return filename
-            .replace(" ", "")
-            .replace("(", "[")
-            .replace(")", "]")
-            .replace("=", "-") // fix trace copying in AndroidX CI
-            .replace(":", "-") // avoid perm error when writing on API 33
+        val index = filename.lastIndexOf('.')
+        return if (index <= 0) {
+            filename.replace(sanitizerRegex, "_")
+        } else {
+            val name = filename.substring(0 until index)
+            val extension = filename.substring(index)
+            val sanitized = name.replace(sanitizerRegex, "_")
+            "$sanitized$extension"
+        }
     }
 
     fun testOutputFile(filename: String): File {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
index a718cec..19d47fc 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
@@ -128,7 +128,9 @@
             .mapKeys { it.key.lowercase() }[name.lowercase()]
 
         fun traceName(traceUniqueName: String, traceTypeLabel: String): String {
-            return "$traceUniqueName-$traceTypeLabel-${dateToFileName()}.trace"
+            return Outputs.sanitizeFilename(
+                "$traceUniqueName-$traceTypeLabel-${dateToFileName()}.trace"
+            )
         }
     }
 }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index 5bde073..3e42bcb 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -227,7 +227,7 @@
         }.toSet()
         val testOutputs = outputs - files
         val trace = testOutputs.singleOrNull { file ->
-            file.absolutePath.endsWith("-method.trace")
+            file.absolutePath.endsWith("method.trace")
         }
         // One method trace should have been created
         assertNotNull(trace)
diff --git a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
index 3b57d3d..8229714 100644
--- a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
@@ -34,7 +34,6 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-@Ignore("b/297916125")
 @OptIn(ExperimentalMetricApi::class)
 class AudioUnderrunBenchmark() {
     @get:Rule
@@ -49,6 +48,7 @@
     }
 
     @Test
+    @Ignore("b/297916125")
     fun start() {
         benchmarkRule.measureRepeated(
             packageName = PACKAGE_NAME,
diff --git a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
index c252b60..cfaa555 100644
--- a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
@@ -29,6 +29,7 @@
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Assume.assumeTrue
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -42,6 +43,7 @@
     private val filterRegex = "^.*L${PACKAGE_NAME.replace(".", "/")}".toRegex()
 
     @Test
+    @Ignore("b/294123161")
     fun appNotInstalled() {
         val error = assertFailsWith<AssertionError> {
             baselineRule.collect(
@@ -56,6 +58,7 @@
     }
 
     @Test
+    @Ignore("b/294123161")
     fun filter() {
         // TODO: share this 'is supported' check with the one inside BaselineProfileRule, once this
         //  test class is moved out of integration-tests, into benchmark-macro-junit4
@@ -90,6 +93,7 @@
     }
 
     @Test
+    @Ignore("b/294123161")
     fun profileType() {
         assumeTrue(Build.VERSION.SDK_INT >= 33 || Shell.isSessionRooted())
 
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index 13659a7..02dd082 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -131,9 +131,9 @@
     field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
     field public static final String EXTRA_COLOR_SCHEME_PARAMS = "androidx.browser.customtabs.extra.COLOR_SCHEME_PARAMS";
     field @Deprecated public static final String EXTRA_DEFAULT_SHARE_MENU_ITEM = "android.support.customtabs.extra.SHARE_MENU_ITEM";
+    field public static final String EXTRA_DISABLE_BACKGROUND_INTERACTION = "androidx.browser.customtabs.extra.DISABLE_BACKGROUND_INTERACTION";
     field public static final String EXTRA_DISABLE_BOOKMARKS_BUTTON = "org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_STAR_BUTTON";
     field public static final String EXTRA_DISABLE_DOWNLOAD_BUTTON = "org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON";
-    field public static final String EXTRA_ENABLE_BACKGROUND_INTERACTION = "androidx.browser.customtabs.extra.ENABLE_BACKGROUND_INTERACTION";
     field public static final String EXTRA_ENABLE_INSTANT_APPS = "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS";
     field public static final String EXTRA_ENABLE_URLBAR_HIDING = "android.support.customtabs.extra.ENABLE_URLBAR_HIDING";
     field public static final String EXTRA_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index ef43681..1e7b718 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -142,9 +142,9 @@
     field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
     field public static final String EXTRA_COLOR_SCHEME_PARAMS = "androidx.browser.customtabs.extra.COLOR_SCHEME_PARAMS";
     field @Deprecated public static final String EXTRA_DEFAULT_SHARE_MENU_ITEM = "android.support.customtabs.extra.SHARE_MENU_ITEM";
+    field public static final String EXTRA_DISABLE_BACKGROUND_INTERACTION = "androidx.browser.customtabs.extra.DISABLE_BACKGROUND_INTERACTION";
     field public static final String EXTRA_DISABLE_BOOKMARKS_BUTTON = "org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_STAR_BUTTON";
     field public static final String EXTRA_DISABLE_DOWNLOAD_BUTTON = "org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON";
-    field public static final String EXTRA_ENABLE_BACKGROUND_INTERACTION = "androidx.browser.customtabs.extra.ENABLE_BACKGROUND_INTERACTION";
     field public static final String EXTRA_ENABLE_INSTANT_APPS = "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS";
     field public static final String EXTRA_ENABLE_URLBAR_HIDING = "android.support.customtabs.extra.ENABLE_URLBAR_HIDING";
     field public static final String EXTRA_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
index df73c25..877a2f5 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
@@ -185,11 +185,11 @@
             "androidx.browser.customtabs.extra.TRANSLATE_LANGUAGE_TAG";
 
     /**
-     * Extra that, when set to false, disables interactions with the background app
-     * when a Partial Custom Tab is launched.
+     * Extra tha disables interactions with the background app when a Partial Custom Tab
+     * is launched.
      */
-    public static final String EXTRA_ENABLE_BACKGROUND_INTERACTION =
-            "androidx.browser.customtabs.extra.ENABLE_BACKGROUND_INTERACTION";
+    public static final String EXTRA_DISABLE_BACKGROUND_INTERACTION =
+            "androidx.browser.customtabs.extra.DISABLE_BACKGROUND_INTERACTION";
 
     /**
      * Extra that enables the client to add an additional action button to the toolbar.
@@ -1173,11 +1173,11 @@
          * Enables the interactions with the background app when a Partial Custom Tab is launched.
          *
          * @param enabled Whether the background interaction is enabled.
-         * @see CustomTabsIntent#EXTRA_ENABLE_BACKGROUND_INTERACTION
+         * @see CustomTabsIntent#EXTRA_DISABLE_BACKGROUND_INTERACTION
          */
         @NonNull
         public Builder setBackgroundInteractionEnabled(boolean enabled) {
-            mIntent.putExtra(EXTRA_ENABLE_BACKGROUND_INTERACTION, enabled);
+            mIntent.putExtra(EXTRA_DISABLE_BACKGROUND_INTERACTION, !enabled);
             return this;
         }
 
@@ -1456,10 +1456,10 @@
 
     /**
      * @return Whether the background interaction is enabled.
-     * @see CustomTabsIntent#EXTRA_ENABLE_BACKGROUND_INTERACTION
+     * @see CustomTabsIntent#EXTRA_DISABLE_BACKGROUND_INTERACTION
      */
     public static boolean isBackgroundInteractionEnabled(@NonNull Intent intent) {
-        return intent.getBooleanExtra(EXTRA_ENABLE_BACKGROUND_INTERACTION, false);
+        return !intent.getBooleanExtra(EXTRA_DISABLE_BACKGROUND_INTERACTION, false);
     }
 
     /**
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
index 08dea64..aee0f19 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
@@ -586,16 +586,17 @@
     @Test
     public void testBackgroundInteraction() {
         Intent intent = new CustomTabsIntent.Builder().build().intent;
-        assertFalse(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
+        assertTrue(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
 
         intent = new CustomTabsIntent.Builder()
-                .setBackgroundInteractionEnabled(false).build().intent;
-        assertFalse(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
-
-        // The extra is set to true only when explicitly called to enable it.
-        intent = new CustomTabsIntent.Builder()
                 .setBackgroundInteractionEnabled(true).build().intent;
         assertTrue(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
+
+        // The extra (EXTRA_DISABLE_BACKGROUND_INTERACTION) is set to true
+        // only when explicitly called to disable it.
+        intent = new CustomTabsIntent.Builder()
+                .setBackgroundInteractionEnabled(false).build().intent;
+        assertFalse(CustomTabsIntent.isBackgroundInteractionEnabled(intent));
     }
 
     @Test
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index a56b608..5a881a2 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -367,9 +367,6 @@
         disable.add("RestrictedApi")
         fatal.add("RestrictedApiAndroidX")
 
-        // Disable until ag/19949626 goes in (b/261918265)
-        disable.add("MissingQuantity")
-
         // Provide stricter enforcement for project types intended to run on a device.
         if (extension.type.compilationTarget == CompilationTarget.DEVICE) {
             fatal.add("Assert")
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirk.kt
index 9dfecbd..46ab296 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirk.kt
@@ -26,11 +26,12 @@
  * Quirks that denotes the device has a slow flash sequence that could result in blurred pictures.
  *
  * QuirkSummary
- * - Bug Id: 211474332, 286190938, 280221967
+ * - Bug Id: 211474332, 286190938, 280221967, 296814664, 296816175
  * - Description: When capturing still photos in auto flash mode, it needs more than 1 second to
  *   flash or capture actual photo after flash, and therefore it easily results in blurred or dark
  *   or overexposed pictures.
- * - Device(s): Pixel 3a / Pixel 3a XL, all models of Pixel 4 and 5, SM-A320
+ * - Device(s): Pixel 3a / Pixel 3a XL, all models of Pixel 4 and 5, SM-A320, Moto G20, Itel A48,
+ *   Realme C11 2021
  *
  * TODO(b/270421716): enable CameraXQuirksClassDetector lint check when kotlin is supported.
  */
@@ -44,7 +45,10 @@
             "PIXEL 3A XL",
             "PIXEL 4", // includes Pixel 4 XL, 4A, and 4A (5g) too
             "PIXEL 5", // includes Pixel 5A too
-            "SM-A320"
+            "SM-A320",
+            "MOTO G(20)",
+            "ITEL L6006", // Itel A48
+            "RMX3231" // Realme C11 2021
         )
 
         fun isEnabled(cameraMetadata: CameraMetadata): Boolean {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.kt
index 836663e..553c96f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.kt
@@ -25,7 +25,7 @@
  * QuirkSummary
  * - Bug Id: 228272227
  * - Description: The Torch is unexpectedly turned off after taking a picture.
- * - Device(s): Redmi 4X, Redmi 5A, Mi A1, Mi A2, Mi A2 lite and Redmi 6 Pro.
+ * - Device(s): Redmi 4X, Redmi 5A, Redmi Note 5, Mi A1, Mi A2, Mi A2 lite and Redmi 6 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
@@ -39,6 +39,7 @@
             "mi a2 lite", // Xiaomi Mi A2 Lite
             "redmi 4x", // Xiaomi Redmi 4X
             "redmi 5a", // Xiaomi Redmi 5A
+            "redmi note 5", // Xiaomi Redmi Note 5
             "redmi 6 pro", // Xiaomi Redmi 6 Pro
         )
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirkTest.kt
index fd8fa84..d8f0400 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashTooSlowQuirkTest.kt
@@ -58,6 +58,9 @@
             arrayOf("sm-a320f", CameraCharacteristics.LENS_FACING_BACK, true),
             arrayOf("SM-A320FL", CameraCharacteristics.LENS_FACING_BACK, true),
             arrayOf("Samsung S7", CameraCharacteristics.LENS_FACING_BACK, false),
+            arrayOf("moto g(20)", CameraCharacteristics.LENS_FACING_BACK, true),
+            arrayOf("itel l6006", CameraCharacteristics.LENS_FACING_BACK, true),
+            arrayOf("rmx3231", CameraCharacteristics.LENS_FACING_BACK, true),
         )
     }
 
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.kt
index da2185d..41d192f 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.kt
@@ -52,9 +52,11 @@
 import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit
 import kotlin.math.ceil
+import org.hamcrest.CoreMatchers.equalTo
 import org.junit.After
 import org.junit.AfterClass
 import org.junit.Assume
+import org.junit.Assume.assumeThat
 import org.junit.Before
 import org.junit.BeforeClass
 import org.junit.Rule
@@ -89,7 +91,7 @@
         // Release camera, otherwise the CameraDevice is not closed, which can cause problems that
         // interfere with other tests.
         if (camera2CameraImpl != null) {
-            camera2CameraImpl!!.release().get()
+            camera2CameraImpl!!.release().get(5, TimeUnit.SECONDS)
             camera2CameraImpl = null
         }
 
@@ -452,6 +454,92 @@
         ).isFalse()
     }
 
+    @Test
+    fun openCameraExceptionWithoutOnError_successReopenCamera() {
+        val errorTimeout = 2000L
+
+        // Arrange. Set up the camera
+        val cameraManagerImpl = FailCameraOpenCameraManagerImpl().apply {
+            shouldEmitExceptionWithoutOnError = true
+        }
+        setUpCamera(cameraManagerImpl)
+
+        val cameraOpenStateSemaphore = Semaphore(0)
+        val stateObserver = object : Observable.Observer<CameraInternal.State?> {
+            override fun onNewData(newState: CameraInternal.State?) {
+                if (newState == CameraInternal.State.OPEN) {
+                    cameraOpenStateSemaphore.release()
+                }
+            }
+
+            override fun onError(t: Throwable) {
+                Logger.e("CameraReopenTest", "Camera state error: " + t.message)
+            }
+        }
+        camera2CameraImpl!!.cameraState.addObserver(
+            CameraXExecutors.directExecutor(), stateObserver
+        )
+        // Act. Try opening the camera. This will fail and trigger reopening.
+        camera2CameraImpl!!.open()
+
+        // Assume the openCamera() should be called.
+        assumeThat(
+            "The test should not able to open camera directly",
+            cameraOpenStateSemaphore.tryAcquire(errorTimeout, TimeUnit.MILLISECONDS),
+            equalTo(false)
+        )
+        camera2CameraImpl!!.cameraState.removeObserver(stateObserver)
+
+        // Allow camera opening to succeed
+        cameraManagerImpl.apply {
+            shouldFailCameraOpen = false
+            shouldEmitExceptionWithoutOnError = false
+        }
+        // Assert. Verify the camera opens
+        awaitCameraState(
+            CameraInternal.State.OPEN,
+            errorTimeout + REOPEN_DELAY_MS + WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS
+        )
+    }
+
+    @Test
+    fun openCameraExceptionWithoutOnError_cameraReleaseBeforeReopen() {
+        // Arrange. Set up the camera
+        val cameraManagerImpl = FailCameraOpenCameraManagerImpl().apply {
+            shouldEmitExceptionWithoutOnError = true
+        }
+        setUpCamera(cameraManagerImpl)
+
+        // Try opening the camera. This will fail.
+        camera2CameraImpl!!.open()
+        awaitCameraState(CameraInternal.State.OPENING, 1000)
+
+        // Act. release camera2CameraImpl
+        camera2CameraImpl!!.release()
+
+        // Assert. Verify the camera switches to the RELEASED state.
+        awaitCameraState(CameraInternal.State.RELEASED, 1000)
+    }
+
+    @Test
+    fun openCameraExceptionWithoutOnError_cameraCloseBeforeReopen() {
+        // Arrange. Set up the camera
+        val cameraManagerImpl = FailCameraOpenCameraManagerImpl().apply {
+            shouldEmitExceptionWithoutOnError = true
+        }
+        setUpCamera(cameraManagerImpl)
+
+        // Try opening the camera. This will fail.
+        camera2CameraImpl!!.open()
+        awaitCameraState(CameraInternal.State.OPENING, 1000)
+
+        // Act. close camera2CameraImpl
+        camera2CameraImpl!!.close()
+
+        // Assert. Verify the camera switches to the CLOSED state.
+        awaitCameraState(CameraInternal.State.CLOSED, 1000)
+    }
+
     @Throws(CameraAccessExceptionCompat::class, CameraUnavailableException::class)
     private fun setUpCamera(cameraManagerImpl: FailCameraOpenCameraManagerImpl) {
         // Build camera manager wrapper
@@ -487,8 +575,10 @@
         awaitCameraState(CameraInternal.State.PENDING_OPEN)
     }
 
-    @Throws(InterruptedException::class)
-    private fun awaitCameraState(state: CameraInternal.State) {
+    private fun awaitCameraState(
+        state: CameraInternal.State,
+        timeoutMs: Long = WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong()
+    ) {
         val cameraStateSemaphore = Semaphore(0)
         val observer: Observable.Observer<CameraInternal.State?> =
             object : Observable.Observer<CameraInternal.State?> {
@@ -508,10 +598,7 @@
         )
         try {
             Truth.assertThat(
-                cameraStateSemaphore.tryAcquire(
-                    WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
-                    TimeUnit.MILLISECONDS
-                )
+                cameraStateSemaphore.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)
             ).isTrue()
         } finally {
             camera2CameraImpl!!.cameraState.removeObserver(observer)
@@ -608,6 +695,9 @@
     @Volatile
     var shouldEmitCameraInUseError = false
 
+    @Volatile
+    var shouldEmitExceptionWithoutOnError = false
+
     @GuardedBy("lock")
     var deviceStateCallback: CameraDeviceCallbackWrapper? = null
         get() {
@@ -665,7 +755,9 @@
                 onCameraOpenAttemptListener!!.onCameraOpenAttempt()
             }
             if (shouldFailCameraOpen) {
-                if (shouldEmitCameraInUseError) {
+                if (shouldEmitExceptionWithoutOnError) {
+                    throw CameraAccessExceptionCompat(2)
+                } else if (shouldEmitCameraInUseError) {
                     executor.execute {
                         callback.onError(
                             mock(CameraDevice::class.java),
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
index 4f491d0..bfbb8c6 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
@@ -960,6 +960,32 @@
     }
 
     @Test
+    public void cameraOnError_closeDeferrableSurfaces() throws InterruptedException {
+        mCaptureSessionOpenerBuilder = new SynchronizedCaptureSession.OpenerBuilder(mExecutor,
+                mScheduledExecutor, mHandler, mCaptureSessionRepository,
+                new Quirks(Collections.emptyList()), DeviceQuirks.getAll());
+
+        CaptureSession captureSession = createCaptureSession();
+        captureSession.setSessionConfig(mTestParameters0.mSessionConfig);
+
+        captureSession.open(mTestParameters0.mSessionConfig, mCameraDeviceHolder.get(),
+                mCaptureSessionOpenerBuilder.build());
+
+        assertTrue(mTestParameters0.waitForData());
+
+        Runnable runnable = mock(Runnable.class);
+        mTestParameters0.mDeferrableSurface.getTerminationFuture().addListener(runnable,
+                CameraXExecutors.directExecutor());
+
+        // Act. Simulate CameraDevice.StateCallback#onError
+        mCaptureSessionRepository.getCameraStateCallback().onError(mCameraDeviceHolder.get(),
+                CameraDevice.StateCallback.ERROR_CAMERA_SERVICE);
+
+        // Assert. Verify DeferrableSurfaces are closed.
+        Mockito.verify(runnable, timeout(3000).times(1)).run();
+    }
+
+    @Test
     public void closingCaptureSessionClosesDeferrableSurface()
             throws ExecutionException, InterruptedException {
         mCaptureSessionOpenerBuilder = new SynchronizedCaptureSession.OpenerBuilder(mExecutor,
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 60edc41..78ff31d 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
@@ -21,7 +21,6 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.SurfaceTexture;
-import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CameraManager;
@@ -104,7 +103,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -216,6 +215,9 @@
     @NonNull
     private final SupportedSurfaceCombination mSupportedSurfaceCombination;
 
+    private final ErrorTimeoutReopenScheduler
+            mErrorTimeoutReopenScheduler = new ErrorTimeoutReopenScheduler();
+
     /**
      * Constructor for a camera.
      *
@@ -373,7 +375,9 @@
                 break;
             case OPENING:
             case REOPENING:
-                boolean canFinish = mStateCallback.cancelScheduledReopen();
+                boolean canFinish = mStateCallback.cancelScheduledReopen()
+                        || mErrorTimeoutReopenScheduler.isErrorHandling();
+                mErrorTimeoutReopenScheduler.cancel();
                 setState(InternalState.CLOSING);
                 if (canFinish) {
                     Preconditions.checkState(isSessionCloseComplete());
@@ -541,7 +545,9 @@
             case CLOSING:
             case REOPENING:
             case RELEASING:
-                boolean canFinish = mStateCallback.cancelScheduledReopen();
+                boolean canFinish = mStateCallback.cancelScheduledReopen()
+                        || mErrorTimeoutReopenScheduler.isErrorHandling();
+                mErrorTimeoutReopenScheduler.cancel();
                 // Wait for the camera async callback to finish releasing
                 setState(InternalState.RELEASING);
                 if (canFinish) {
@@ -605,6 +611,7 @@
                     case CLOSING:
                     case RELEASING:
                         if (isSessionCloseComplete() && mCameraDevice != null) {
+                            debugLog("closing camera");
                             ApiCompat.Api21Impl.close(mCameraDevice);
                             mCameraDevice = null;
                         }
@@ -1256,6 +1263,7 @@
             mStateCallback.resetReopenMonitor();
         }
         mStateCallback.cancelScheduledReopen();
+        mErrorTimeoutReopenScheduler.cancel();
 
         debugLog("Opening camera.");
         setState(InternalState.OPENING);
@@ -1275,6 +1283,9 @@
                 default:
                     // Camera2 will call the onError() callback with the specific error code that
                     // caused this failure. No need to do anything here.
+
+                    // Certain devices may not call onError(), please see b/290861504#comment3
+                    mErrorTimeoutReopenScheduler.start();
             }
         } catch (SecurityException e) {
             debugLog("Unable to open camera due to " + e.getMessage());
@@ -1289,6 +1300,92 @@
         }
     }
 
+    /**
+     * Reopen the Camera if CameraDevice.StateCallback#onError be called within a reasonable delay.
+     */
+    private class ErrorTimeoutReopenScheduler {
+
+        private static final long ERROR_TIMEOUT_MILLIS = 2000;
+
+        @Nullable
+        private ScheduleNode mScheduleNode = null;
+
+        @ExecutedBy("mExecutor")
+        public void start() {
+            if (mState != InternalState.OPENING) {
+                debugLog("Don't need the onError timeout handler.");
+                return;
+            }
+
+            debugLog("Camera waiting for onError.");
+            cancel();
+            mScheduleNode = new ScheduleNode();
+        }
+
+        /**
+         * @return True if CameraManager#openCamera throws an exception but still does not
+         * receive onError callback. Otherwise false.
+         */
+        @ExecutedBy("mExecutor")
+        public boolean isErrorHandling() {
+            return mScheduleNode != null && !mScheduleNode.isDone();
+        }
+
+        @ExecutedBy("mExecutor")
+        public void deviceOnError() {
+            debugLog("Camera receive onErrorCallback");
+            cancel();
+        }
+
+        @ExecutedBy("mExecutor")
+        public void cancel() {
+            if (mScheduleNode != null) {
+                mScheduleNode.cancel();
+            }
+            mScheduleNode = null;
+        }
+
+        private class ScheduleNode {
+            private final ScheduledFuture<?> mScheduledFuture;
+            private final AtomicBoolean mIsDone = new AtomicBoolean(false);
+            ScheduleNode() {
+                mScheduledFuture = mScheduledExecutorService.schedule(this::execute,
+                        ERROR_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+            }
+
+            private void execute() {
+                if (mIsDone.getAndSet(true)) {
+                    return;
+                }
+
+                mExecutor.execute(this::executeInternal);
+            }
+
+            @ExecutedBy("mExecutor")
+            private void executeInternal() {
+                if (mState != InternalState.OPENING) {
+                    debugLog("Camera skip reopen at state: " + mState);
+                    return;
+                }
+
+                debugLog("Camera onError timeout, reopen it.");
+                setState(InternalState.REOPENING);
+                mStateCallback.scheduleCameraReopen();
+            }
+
+            @ExecutedBy("mExecutor")
+            public void cancel() {
+                mIsDone.set(true);
+                mScheduledFuture.cancel(true);
+            }
+
+            @ExecutedBy("mExecutor")
+            public boolean isDone() {
+                return mIsDone.get();
+            }
+        }
+    }
+
     /** Updates the capture request configuration for the current capture session. */
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @ExecutedBy("mExecutor")
@@ -1362,22 +1459,16 @@
             @ExecutedBy("mExecutor")
             public void onFailure(@NonNull Throwable t) {
                 if (t instanceof DeferrableSurface.SurfaceClosedException) {
-                    SessionConfig sessionConfig =
-                            findSessionConfigForSurface(
-                                    ((DeferrableSurface.SurfaceClosedException) t)
-                                            .getDeferrableSurface());
+                    SessionConfig sessionConfig = findSessionConfigForSurface(
+                            ((DeferrableSurface.SurfaceClosedException) t).getDeferrableSurface());
                     if (sessionConfig != null) {
                         postSurfaceClosedError(sessionConfig);
                     }
                     return;
                 }
-
-                // A CancellationException is thrown when (1) A CaptureSession is closed while it
-                // is opening. In this case, another CaptureSession should be opened shortly
-                // after or (2) When opening a CaptureSession fails.
-                // TODO(b/183504720): Distinguish between both scenarios, and communicate the
-                //  second one to the developer.
                 if (t instanceof CancellationException) {
+                    // A CancellationException is thrown when a CaptureSession is closed while it
+                    // is opening. In this case, another CaptureSession should be opened shortly.
                     debugLog("Unable to configure camera cancelled");
                     return;
                 }
@@ -1388,13 +1479,8 @@
                             CameraState.StateError.create(CameraState.ERROR_STREAM_CONFIG, t));
                 }
 
-                if (t instanceof CameraAccessException) {
-                    debugLog("Unable to configure camera due to " + t.getMessage());
-                } else if (t instanceof TimeoutException) {
-                    // TODO: Consider to handle the timeout error.
-                    Logger.e(TAG, "Unable to configure camera " + mCameraInfoInternal.getCameraId()
-                            + ", timeout!");
-                }
+                Logger.e(TAG, "Unable to configure camera " + Camera2CameraImpl.this, t);
+                resetCaptureSession(/*abortInFlightCaptures=*/false);
             }
         }, mExecutor);
     }
@@ -1421,8 +1507,7 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     void postSurfaceClosedError(@NonNull SessionConfig sessionConfig) {
         Executor executor = CameraXExecutors.mainThreadExecutor();
-        List<SessionConfig.ErrorListener> errorListeners =
-                sessionConfig.getErrorListeners();
+        List<SessionConfig.ErrorListener> errorListeners = sessionConfig.getErrorListeners();
         if (!errorListeners.isEmpty()) {
             SessionConfig.ErrorListener errorListener = errorListeners.get(0);
             debugLog("Posting surface closed", new Throwable());
@@ -1883,6 +1968,7 @@
             // during initialization, so keep track of it here.
             mCameraDevice = cameraDevice;
             mCameraDeviceError = error;
+            mErrorTimeoutReopenScheduler.deviceOnError();
 
             switch (mState) {
                 case RELEASING:
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
index 60c74f0..2d66b8c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.interop.CaptureRequestOptions;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -37,6 +38,7 @@
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.StreamSpec;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -111,6 +113,21 @@
 
     }
 
+    @VisibleForTesting
+    static void applyVideoStabilization(@NonNull CaptureConfig captureConfig,
+            @NonNull CaptureRequest.Builder builder) {
+        if (captureConfig.getPreviewStabilizationMode() == StabilizationMode.OFF
+                || captureConfig.getVideoStabilizationMode() == StabilizationMode.OFF) {
+            builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
+                    CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF);
+        } else if (captureConfig.getPreviewStabilizationMode() == StabilizationMode.ON) {
+            builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
+                    CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION);
+        } else if (captureConfig.getVideoStabilizationMode() == StabilizationMode.ON) {
+            builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
+                    CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON);
+        }
+    }
 
     /**
      * Builds a {@link CaptureRequest} from a {@link CaptureConfig} and a {@link CameraDevice}.
@@ -155,6 +172,8 @@
 
         applyAeFpsRange(captureConfig, builder);
 
+        applyVideoStabilization(captureConfig, builder);
+
         if (captureConfig.getImplementationOptions().containsOption(
                 CaptureConfig.OPTION_ROTATION)) {
             builder.set(CaptureRequest.JPEG_ORIENTATION,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
index e64c90c..dc1207c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
@@ -89,6 +89,10 @@
                         camera2Config.getSessionCaptureCallback(
                                 Camera2CaptureCallbacks.createNoOpCallback())));
 
+        // Set video stabilization mode
+        builder.setVideoStabilization(config.getVideoStabilizationMode());
+        builder.setPreviewStabilization(config.getPreviewStabilizationMode());
+
         // Copy extended Camera2 configurations
         MutableOptionsBundle extendedConfig = MutableOptionsBundle.create();
         extendedConfig.insertOption(Camera2ImplConfig.SESSION_PHYSICAL_CAMERA_ID_OPTION,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionInterface.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionInterface.java
index 4e745ce..ac31ea2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionInterface.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionInterface.java
@@ -58,6 +58,11 @@
      * @param opener        The opener to open the {@link SynchronizedCaptureSession}.
      * @return A {@link ListenableFuture} that will be completed once the
      * {@link CameraCaptureSession} has been configured.
+     * It may be set to a {@link java.util.concurrent.CancellationException} if a CaptureSession
+     * is closed while it is opening.
+     * It may be set to a {@link DeferrableSurface.SurfaceClosedException} if any of the supplied
+     * DeferrableSurface is closed that cannot be used to configure the
+     * {@link CameraCaptureSession}.
      */
     @NonNull
     ListenableFuture<Void> open(@NonNull SessionConfig sessionConfig,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionRepository.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionRepository.java
index 31712f3d..0254f2d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionRepository.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionRepository.java
@@ -70,6 +70,7 @@
                     // error state. The CameraCaptureSession.close() may not invoke the onClosed()
                     // callback so it has to finish the close process forcibly.
                     forceOnClosedCaptureSessions();
+                    dispatchOnError(error);
                     cameraClosed();
                 }
 
@@ -104,6 +105,19 @@
                     mExecutor.execute(() -> forceOnClosed(sessions));
                 }
 
+                private void dispatchOnError(int error) {
+                    LinkedHashSet<SynchronizedCaptureSession> sessions = new LinkedHashSet<>();
+                    synchronized (mLock) {
+                        sessions.addAll(mCreatingCaptureSessions);
+                        sessions.addAll(mCaptureSessions);
+                    }
+                    mExecutor.execute(() -> {
+                        for (SynchronizedCaptureSession session : sessions) {
+                            session.onCameraDeviceError(error);
+                        }
+                    });
+                }
+
                 private void cameraClosed() {
                     List<SynchronizedCaptureSession> sessions;
                     synchronized (mLock) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
index e5e07ac..e94866a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
@@ -295,6 +295,14 @@
     void finishClose();
 
     /**
+     * This method should be called when CameraDevice.StateCallback#onError happens.
+     *
+     * <p>It is used to inform the error of the CameraDevice, and should not be called for
+     * other reasons.
+     */
+    void onCameraDeviceError(int error);
+
+    /**
      * A callback object interface to adapting the updates from
      * {@link CameraCaptureSession.StateCallback}.
      *
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
index 9ed6659..bd63367 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
@@ -625,6 +625,11 @@
         releaseDeferrableSurfaces();
     }
 
+    @Override
+    public void onCameraDeviceError(int error) {
+        // Nothing to do for the default implementation.
+    }
+
     /**
      * Nested class to avoid verification errors for methods introduced in Android 6.0 (API 23).
      */
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionImpl.java
index de4b4b8c..4dc098a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionImpl.java
@@ -206,6 +206,22 @@
         mWaitForOtherSessionCompleteQuirk.onFinishClosed();
     }
 
+    @Override
+    public void onCameraDeviceError(int error) {
+        super.onCameraDeviceError(error);
+        if (error == CameraDevice.StateCallback.ERROR_CAMERA_SERVICE) {
+            synchronized (mObjectLock) {
+                if (isCameraCaptureSessionOpen() && mDeferrableSurfaces != null) {
+                    debugLog("Close DeferrableSurfaces for CameraDevice error.");
+                    // b/290861504#comment4, close the DeferrableSurfaces.
+                    for (DeferrableSurface deferrableSurface : mDeferrableSurfaces) {
+                        deferrableSurface.close();
+                    }
+                }
+            }
+        }
+    }
+
     void debugLog(String message) {
         Logger.d(TAG, "[" + SynchronizedCaptureSessionImpl.this + "] " + message);
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirk.java
index 8da23cc..2398ddf 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirk.java
@@ -33,11 +33,12 @@
  * Quirks that denotes the device has a slow flash sequence that could result in blurred pictures.
  *
  * <p>QuirkSummary
- *     Bug Id: 211474332, 286190938, 280221967
+ *     Bug Id: 211474332, 286190938, 280221967, 296814664, 296816175
  *     Description: When capturing still photos in auto flash mode, it needs more than 1 second to
  *     flash or capture actual photo after flash, and therefore it easily results in blurred or dark
  *     or overexposed pictures.
- *     Device(s): Pixel 3a / Pixel 3a XL, all models of Pixel 4 and 5, SM-A320
+ *     Device(s): Pixel 3a / Pixel 3a XL, all models of Pixel 4 and 5, SM-A320, Moto G20, Itel A48,
+ *     Realme C11 2021
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class FlashTooSlowQuirk implements UseTorchAsFlashQuirk {
@@ -46,7 +47,10 @@
             "PIXEL 3A XL",
             "PIXEL 4", // includes Pixel 4 XL, 4A, and 4A (5g) too
             "PIXEL 5", // includes Pixel 5A too
-            "SM-A320"
+            "SM-A320",
+            "MOTO G(20)",
+            "ITEL L6006", // Itel A48
+            "RMX3231" // Realme C11 2021
     );
 
     static boolean load(@NonNull CameraCharacteristicsCompat cameraCharacteristics) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java
index dce664d..cbff938 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java
@@ -29,19 +29,20 @@
  * <p>QuirkSummary
  *     Bug Id: 228272227
  *     Description: The Torch is unexpectedly turned off after taking a picture.
- *     Device(s): Redmi 4X, Redmi 5A, Mi A1, Mi A2, Mi A2 lite and Redmi 6 Pro.
+ *     Device(s): Redmi 4X, Redmi 5A, Redmi Note 5, Mi A1, Mi A2, Mi A2 lite and Redmi 6 Pro.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class TorchIsClosedAfterImageCapturingQuirk implements Quirk {
 
     // List of devices with the issue. See b/228272227.
     public static final List<String> BUILD_MODELS = Arrays.asList(
-            "mi a1",       // Xiaomi Mi A1
-            "mi a2",       // Xiaomi Mi A2
-            "mi a2 lite",  // Xiaomi Mi A2 Lite
-            "redmi 4x",    // Xiaomi Redmi 4X
-            "redmi 5a",    // Xiaomi Redmi 5A
-            "redmi 6 pro"  // Xiaomi Redmi 6 Pro
+            "mi a1",        // Xiaomi Mi A1
+            "mi a2",        // Xiaomi Mi A2
+            "mi a2 lite",   // Xiaomi Mi A2 Lite
+            "redmi 4x",     // Xiaomi Redmi 4X
+            "redmi 5a",     // Xiaomi Redmi 5A
+            "redmi note 5", // Xiaomi Redmi Note 5
+            "redmi 6 pro"   // Xiaomi Redmi 6 Pro
     );
 
     static boolean load() {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
index 76be504..def80e4 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
@@ -34,11 +34,16 @@
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.Preview;
 import androidx.camera.core.impl.CameraCaptureCallback;
+import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.Config.OptionPriority;
 import androidx.camera.core.impl.ImageCaptureConfig;
 import androidx.camera.core.impl.PreviewConfig;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
+import androidx.camera.video.Recorder;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.impl.VideoCaptureConfig;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -169,6 +174,83 @@
                 .isEqualTo(CaptureRequest.TONEMAP_MODE_HIGH_QUALITY);
     }
 
+    @Test
+    public void unpackerExtractsPreviewStabilizationMode() {
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "Google");
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "sunfish");
+
+        Preview.Builder previewConfigBuilder =
+                new Preview.Builder().setPreviewStabilizationEnabled(true);
+
+        PreviewConfig useCaseConfig = previewConfigBuilder.getUseCaseConfig();
+
+        SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
+        mUnpacker.unpack(RESOLUTION_VGA, useCaseConfig, sessionBuilder);
+        SessionConfig sessionConfig = sessionBuilder.build();
+
+        CaptureConfig captureConfig = sessionConfig.getRepeatingCaptureConfig();
+
+        assertThat(captureConfig.getVideoStabilizationMode())
+                .isEqualTo(StabilizationMode.UNSPECIFIED);
+        assertThat(captureConfig.getPreviewStabilizationMode())
+                .isEqualTo(StabilizationMode.ON);
+    }
+
+    @Test
+    public void unpackerExtractsVideoStabilizationMode() {
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "Google");
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "sunfish");
+
+        VideoCapture.Builder<Recorder> videoCaptureConfigBuilder =
+                new VideoCapture.Builder<>(new Recorder.Builder().build())
+                        .setVideoStabilizationEnabled(true);
+
+        VideoCaptureConfig<Recorder> useCaseConfig = videoCaptureConfigBuilder.getUseCaseConfig();
+
+        SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
+        mUnpacker.unpack(RESOLUTION_VGA, useCaseConfig, sessionBuilder);
+        SessionConfig sessionConfig = sessionBuilder.build();
+
+        CaptureConfig captureConfig = sessionConfig.getRepeatingCaptureConfig();
+
+        assertThat(captureConfig.getVideoStabilizationMode())
+                .isEqualTo(StabilizationMode.ON);
+        assertThat(captureConfig.getPreviewStabilizationMode())
+                .isEqualTo(StabilizationMode.UNSPECIFIED);
+    }
+
+    @Test
+    public void unpackerExtractsBothPreviewAndVideoStabilizationMode() {
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "Google");
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "sunfish");
+
+        // unpack for preview
+        Preview.Builder previewConfigBuilder =
+                new Preview.Builder().setPreviewStabilizationEnabled(true);
+
+        PreviewConfig previewConfig = previewConfigBuilder.getUseCaseConfig();
+
+        SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
+        mUnpacker.unpack(RESOLUTION_VGA, previewConfig, sessionBuilder);
+
+        // unpack for preview
+        VideoCapture.Builder<Recorder> videoCaptureConfigBuilder =
+                new VideoCapture.Builder<>(new Recorder.Builder().build())
+                        .setVideoStabilizationEnabled(true);
+
+        VideoCaptureConfig<Recorder> videoCaptureConfig =
+                videoCaptureConfigBuilder.getUseCaseConfig();
+
+        mUnpacker.unpack(RESOLUTION_VGA, videoCaptureConfig, sessionBuilder);
+        SessionConfig sessionConfig = sessionBuilder.build();
+        CaptureConfig captureConfig = sessionConfig.getRepeatingCaptureConfig();
+
+        assertThat(captureConfig.getVideoStabilizationMode())
+                .isEqualTo(StabilizationMode.ON);
+        assertThat(captureConfig.getPreviewStabilizationMode())
+                .isEqualTo(StabilizationMode.ON);
+    }
+
     private OptionPriority getCaptureRequestOptionPriority(Config config,
             CaptureRequest.Key<?> key) {
         Config.Option<?> option = Camera2ImplConfig.createCaptureRequestOption(key);
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirkTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirkTest.kt
index 4f156fc..c2f4239 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirkTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/FlashTooSlowQuirkTest.kt
@@ -57,6 +57,9 @@
             arrayOf("sm-a320f", CameraCharacteristics.LENS_FACING_BACK, true),
             arrayOf("SM-A320FL", CameraCharacteristics.LENS_FACING_BACK, true),
             arrayOf("Samsung S7", CameraCharacteristics.LENS_FACING_BACK, false),
+            arrayOf("moto g(20)", CameraCharacteristics.LENS_FACING_BACK, true),
+            arrayOf("itel l6006", CameraCharacteristics.LENS_FACING_BACK, true),
+            arrayOf("rmx3231", CameraCharacteristics.LENS_FACING_BACK, true),
         )
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index a7686c7..c39b5cb 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -41,6 +41,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_TYPE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_PREVIEW_STABILIZATION_MODE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_FRAME_RATE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
@@ -86,6 +87,7 @@
 import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.TargetConfig;
 import androidx.camera.core.internal.ThreadConfig;
@@ -268,6 +270,7 @@
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
                 streamSpec.getResolution());
         sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
+        sessionConfigBuilder.setPreviewStabilization(config.getPreviewStabilizationMode());
         if (streamSpec.getImplementationOptions() != null) {
             sessionConfigBuilder.addImplementationOptions(streamSpec.getImplementationOptions());
         }
@@ -670,6 +673,14 @@
     }
 
     /**
+     * Returns whether video stabilization is enabled for preview stream.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public boolean isPreviewStabilizationEnabled() {
+        return getCurrentConfig().getPreviewStabilizationMode() == StabilizationMode.ON;
+    }
+
+    /**
      * A interface implemented by the application to provide a {@link Surface} for {@link Preview}.
      *
      * <p> This interface is implemented by the application to provide a {@link Surface}. This
@@ -1148,6 +1159,20 @@
             return this;
         }
 
+        /**
+         * Enable preview stabilization.
+         *
+         * @param enabled True if enable, otherwise false.
+         * @return the current Builder.
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setPreviewStabilizationEnabled(boolean enabled) {
+            getMutableConfig().insertOption(OPTION_PREVIEW_STABILIZATION_MODE,
+                    enabled ? StabilizationMode.ON : StabilizationMode.OFF);
+            return this;
+        }
+
         // Implementations of UseCaseConfig.Builder default methods
 
         @RestrictTo(Scope.LIBRARY_GROUP)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureConfig.java
index 6cf83bf..600629c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureConfig.java
@@ -25,6 +25,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -78,6 +79,12 @@
 
     final Range<Integer> mExpectedFrameRateRange;
 
+    @StabilizationMode.Mode
+    final int mPreviewStabilizationMode;
+
+    @StabilizationMode.Mode
+    final int mVideoStabilizationMode;
+
     /** The camera capture callback for a {@link CameraCaptureSession}. */
     final List<CameraCaptureCallback> mCameraCaptureCallbacks;
 
@@ -105,6 +112,9 @@
      * @param templateType           The template for parameters of the CaptureRequest. This
      *                               must match the
      *                               constants defined by {@link CameraDevice}.
+     * @param expectedFrameRateRange The expected frame rate range.
+     * @param previewStabilizationMode The preview stabilization mode.
+     * @param videoStabilizationMode The video stabilization mode.
      * @param cameraCaptureCallbacks All camera capture callbacks.
      * @param cameraCaptureResult     The {@link CameraCaptureResult} for reprocessing capture
      *                               request.
@@ -114,6 +124,8 @@
             Config implementationOptions,
             int templateType,
             @NonNull Range<Integer> expectedFrameRateRange,
+            int previewStabilizationMode,
+            int videoStabilizationMode,
             List<CameraCaptureCallback> cameraCaptureCallbacks,
             boolean useRepeatingSurface,
             @NonNull TagBundle tagBundle,
@@ -122,6 +134,8 @@
         mImplementationOptions = implementationOptions;
         mTemplateType = templateType;
         mExpectedFrameRateRange = expectedFrameRateRange;
+        mPreviewStabilizationMode = previewStabilizationMode;
+        mVideoStabilizationMode = videoStabilizationMode;
         mCameraCaptureCallbacks = Collections.unmodifiableList(cameraCaptureCallbacks);
         mUseRepeatingSurface = useRepeatingSurface;
         mTagBundle = tagBundle;
@@ -169,6 +183,16 @@
         return mExpectedFrameRateRange;
     }
 
+    @StabilizationMode.Mode
+    public int getPreviewStabilizationMode() {
+        return mPreviewStabilizationMode;
+    }
+
+    @StabilizationMode.Mode
+    public int getVideoStabilizationMode() {
+        return mVideoStabilizationMode;
+    }
+
     public boolean isUseRepeatingSurface() {
         return mUseRepeatingSurface;
     }
@@ -206,6 +230,10 @@
         private MutableConfig mImplementationOptions = MutableOptionsBundle.create();
         private int mTemplateType = TEMPLATE_TYPE_NONE;
         private Range<Integer> mExpectedFrameRateRange = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
+        @StabilizationMode.Mode
+        private int mPreviewStabilizationMode = StabilizationMode.UNSPECIFIED;
+        @StabilizationMode.Mode
+        private int mVideoStabilizationMode = StabilizationMode.UNSPECIFIED;
         private List<CameraCaptureCallback> mCameraCaptureCallbacks = new ArrayList<>();
         private boolean mUseRepeatingSurface = false;
         private MutableTagBundle mMutableTagBundle = MutableTagBundle.create();
@@ -220,6 +248,8 @@
             mImplementationOptions = MutableOptionsBundle.from(base.mImplementationOptions);
             mTemplateType = base.mTemplateType;
             mExpectedFrameRateRange = base.mExpectedFrameRateRange;
+            mVideoStabilizationMode = base.mVideoStabilizationMode;
+            mPreviewStabilizationMode = base.mPreviewStabilizationMode;
             mCameraCaptureCallbacks.addAll(base.getCameraCaptureCallbacks());
             mUseRepeatingSurface = base.isUseRepeatingSurface();
             mMutableTagBundle = MutableTagBundle.from(base.getTagBundle());
@@ -290,6 +320,26 @@
         }
 
         /**
+         * Set the preview stabilization mode of the CaptureConfig.
+         * @param mode {@link StabilizationMode}
+         */
+        public void setPreviewStabilization(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mPreviewStabilizationMode = mode;
+            }
+        }
+
+        /**
+         * Set the video stabilization mode of the CaptureConfig.
+         * @param mode {@link StabilizationMode}
+         */
+        public void setVideoStabilization(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mVideoStabilizationMode = mode;
+            }
+        }
+
+        /**
          * Adds a {@link CameraCaptureCallback} callback.
          */
         public void addCameraCaptureCallback(@NonNull CameraCaptureCallback cameraCaptureCallback) {
@@ -416,6 +466,8 @@
                     OptionsBundle.from(mImplementationOptions),
                     mTemplateType,
                     mExpectedFrameRateRange,
+                    mPreviewStabilizationMode,
+                    mVideoStabilizationMode,
                     new ArrayList<>(mCameraCaptureCallbacks),
                     mUseRepeatingSurface,
                     TagBundle.from(mMutableTagBundle),
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
index fdd0390..a3d1fd8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
@@ -29,6 +29,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.DynamicRange;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.internal.compat.workaround.SurfaceSorter;
 
 import com.google.auto.value.AutoValue;
@@ -431,6 +432,30 @@
         }
 
         /**
+         * Set the preview stabilization mode of the SessionConfig.
+         * @param mode {@link StabilizationMode}
+         */
+        @NonNull
+        public Builder setPreviewStabilization(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mCaptureConfigBuilder.setPreviewStabilization(mode);
+            }
+            return this;
+        }
+
+        /**
+         * Set the video stabilization mode of the SessionConfig.
+         * @param mode {@link StabilizationMode}
+         */
+        @NonNull
+        public Builder setVideoStabilization(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mCaptureConfigBuilder.setVideoStabilization(mode);
+            }
+            return this;
+        }
+
+        /**
          * Adds a tag to the SessionConfig with a key. For tracking the source.
          */
         @NonNull
@@ -748,6 +773,8 @@
             }
 
             setOrVerifyExpectFrameRateRange(captureConfig.getExpectedFrameRateRange());
+            setPreviewStabilizationMode(captureConfig.getPreviewStabilizationMode());
+            setVideoStabilizationMode(captureConfig.getVideoStabilizationMode());
 
             TagBundle tagBundle = sessionConfig.getRepeatingCaptureConfig().getTagBundle();
             mCaptureConfigBuilder.addAllTags(tagBundle);
@@ -810,6 +837,18 @@
             }
         }
 
+        private void setPreviewStabilizationMode(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mCaptureConfigBuilder.setPreviewStabilization(mode);
+            }
+        }
+
+        private void setVideoStabilizationMode(@StabilizationMode.Mode int mode) {
+            if (mode != StabilizationMode.UNSPECIFIED) {
+                mCaptureConfigBuilder.setVideoStabilization(mode);
+            }
+        }
+
         private List<DeferrableSurface> getSurfaces() {
             List<DeferrableSurface> surfaces = new ArrayList<>();
             for (OutputConfig outputConfig : mOutputConfigs) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 7e13965..435c05e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -24,6 +24,7 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ExtendableBuilder;
 import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.internal.TargetConfig;
 import androidx.camera.core.internal.UseCaseEventConfig;
 
@@ -100,6 +101,17 @@
     Option<UseCaseConfigFactory.CaptureType> OPTION_CAPTURE_TYPE = Option.create(
             "camerax.core.useCase.captureType", UseCaseConfigFactory.CaptureType.class);
 
+    /**
+     * Option: camerax.core.useCase.previewStabilizationMode
+     */
+    Option<Integer> OPTION_PREVIEW_STABILIZATION_MODE =
+            Option.create("camerax.core.useCase.previewStabilizationMode", int.class);
+
+    /**
+     * Option: camerax.core.useCase.videoStabilizationMode
+     */
+    Option<Integer> OPTION_VIDEO_STABILIZATION_MODE =
+            Option.create("camerax.core.useCase.videoStabilizationMode", int.class);
 
     // *********************************************************************************************
 
@@ -328,6 +340,23 @@
     }
 
     /**
+     * @return The preview stabilization mode of this UseCaseConfig.
+     */
+    @StabilizationMode.Mode
+    default int getPreviewStabilizationMode() {
+        return retrieveOption(OPTION_PREVIEW_STABILIZATION_MODE,
+                StabilizationMode.UNSPECIFIED);
+    }
+
+    /**
+     * @return The video stabilization mode of this UseCaseConfig.
+     */
+    @StabilizationMode.Mode
+    default int getVideoStabilizationMode() {
+        return retrieveOption(OPTION_VIDEO_STABILIZATION_MODE, StabilizationMode.UNSPECIFIED);
+    }
+
+    /**
      * Builder for a {@link UseCase}.
      *
      * @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/stabilization/StabilizationMode.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/stabilization/StabilizationMode.java
new file mode 100644
index 0000000..f5aab65
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/stabilization/StabilizationMode.java
@@ -0,0 +1,51 @@
+/*
+ * 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.camera.core.impl.stabilization;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class for preview or video stabilization mode.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21)
+public class StabilizationMode {
+
+    /* Not specified */
+    public static final int UNSPECIFIED = 0;
+    /* Off */
+    public static final int OFF = 1;
+    /* On */
+    public static final int ON = 2;
+
+    private StabilizationMode() {
+    }
+
+    /**
+     *
+     */
+    @IntDef({UNSPECIFIED, OFF, ON})
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public @interface Mode {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/futures/Futures.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/futures/Futures.java
index dde1276..4410d11 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/futures/Futures.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/futures/Futures.java
@@ -355,6 +355,7 @@
             mCallback.onSuccess(value);
         }
 
+        @NonNull
         @Override
         public String toString() {
             return getClass().getSimpleName() + "," + mCallback;
@@ -428,11 +429,14 @@
             @NonNull ListenableFuture<V> input) {
         return CallbackToFutureAdapter.getFuture(completer -> {
             propagate(input, completer);
-            ScheduledFuture<?> timeoutFuture = scheduledExecutor.schedule(
-                    () -> completer.setException(new TimeoutException("Future[" + input + "] is "
-                            + "not done within " + timeoutMillis + " ms.")),
-                    timeoutMillis, TimeUnit.MILLISECONDS);
-            input.addListener(() -> timeoutFuture.cancel(true), CameraXExecutors.directExecutor());
+            if (!input.isDone()) {
+                ScheduledFuture<?> timeoutFuture = scheduledExecutor.schedule(
+                        () -> completer.setException(new TimeoutException("Future[" + input + "] "
+                                + "is not done within " + timeoutMillis + " ms.")),
+                        timeoutMillis, TimeUnit.MILLISECONDS);
+                input.addListener(
+                        () -> timeoutFuture.cancel(true), CameraXExecutors.directExecutor());
+            }
             return "TimeoutFuture[" + input + "]";
         });
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
index 33a2a1e..7147eb4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
@@ -21,7 +21,9 @@
 import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_PREVIEW_STABILIZATION_MODE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_VIDEO_STABILIZATION_MODE;
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.camera.core.impl.utils.TransformUtils.getRotatedSize;
 import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
@@ -57,6 +59,7 @@
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.processing.SurfaceEdge;
 import androidx.camera.core.processing.SurfaceProcessorNode.OutConfig;
 
@@ -158,6 +161,21 @@
                     + " a dynamic range that satisfies all children.");
         }
         mutableConfig.insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
+
+        // Merge Preview stabilization and video stabilization configs.
+        for (UseCase useCase : mChildren) {
+            if (useCase.getCurrentConfig().getVideoStabilizationMode()
+                    != StabilizationMode.UNSPECIFIED) {
+                mutableConfig.insertOption(OPTION_VIDEO_STABILIZATION_MODE,
+                        useCase.getCurrentConfig().getVideoStabilizationMode());
+            }
+
+            if (useCase.getCurrentConfig().getPreviewStabilizationMode()
+                    != StabilizationMode.UNSPECIFIED) {
+                mutableConfig.insertOption(OPTION_PREVIEW_STABILIZATION_MODE,
+                        useCase.getCurrentConfig().getPreviewStabilizationMode());
+            }
+        }
     }
 
     void bindChildren() {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 57850a9..4d2bc37 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -780,6 +780,13 @@
         assertThat(preview.targetFrameRate).isEqualTo(Range(15, 30))
     }
 
+    @Test
+    fun canSetPreviewStabilization() {
+        val preview = Preview.Builder().setPreviewStabilizationEnabled(true)
+            .build()
+        assertThat(preview.isPreviewStabilizationEnabled).isTrue()
+    }
+
     private fun bindToLifecycleAndGetSurfaceRequest(): SurfaceRequest {
         return bindToLifecycleAndGetResult(null).first
     }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index 3b5c6ea..3743f65 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -31,6 +31,7 @@
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
 import androidx.camera.core.ImageProxy
+import androidx.camera.core.Preview
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.CameraCaptureCallback
 import androidx.camera.core.impl.CameraCaptureResult
@@ -41,6 +42,7 @@
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.stabilization.StabilizationMode
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.impl.utils.futures.Futures
@@ -55,6 +57,8 @@
 import androidx.camera.testing.impl.fakes.FakeUseCase
 import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
 import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory
+import androidx.camera.video.Recorder
+import androidx.camera.video.VideoCapture
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.ListenableFuture
 import kotlinx.coroutines.CompletableDeferred
@@ -479,4 +483,34 @@
         assertThat(config.captureTypes[0]).isEqualTo(CaptureType.PREVIEW)
         assertThat(config.captureTypes[1]).isEqualTo(CaptureType.PREVIEW)
     }
+
+    @Test
+    fun getParentPreviewStabilizationMode_isPreviewChildMode() {
+        val preview = Preview.Builder().setPreviewStabilizationEnabled(true).build()
+        val videoCapture = VideoCapture.Builder(Recorder.Builder().build())
+            .setVideoStabilizationEnabled(false).build()
+
+        streamSharing =
+            StreamSharing(camera, setOf(preview, videoCapture), useCaseConfigFactory)
+        assertThat(
+            streamSharing.mergeConfigs(
+                camera.cameraInfoInternal, /*extendedConfig*/null, /*cameraDefaultConfig*/null
+            ).previewStabilizationMode
+        ).isEqualTo(StabilizationMode.ON)
+    }
+
+    @Test
+    fun getParentVideoStabilizationMode_isVideoCaptureChildMode() {
+        val preview = Preview.Builder().setPreviewStabilizationEnabled(false).build()
+        val videoCapture = VideoCapture.Builder(Recorder.Builder().build())
+            .setVideoStabilizationEnabled(true).build()
+
+        streamSharing =
+            StreamSharing(camera, setOf(preview, videoCapture), useCaseConfigFactory)
+        assertThat(
+            streamSharing.mergeConfigs(
+                camera.cameraInfoInternal, /*extendedConfig*/null, /*cameraDefaultConfig*/null
+            ).videoStabilizationMode
+        ).isEqualTo(StabilizationMode.ON)
+    }
 }
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
index 76a5726..d284439 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.camera.effects.opengl.GlRenderer;
 
 /**
@@ -32,6 +33,7 @@
  * <p>The frame can be empty or filled. A filled frame contains valid information on how to
  * render it. An empty frame can be filled with new content.
  */
+@RequiresApi(21)
 class TextureFrame {
 
     private static final long NO_VALUE = Long.MIN_VALUE;
@@ -41,8 +43,9 @@
     private long mTimestampNs = NO_VALUE;
     @Nullable
     private Surface mSurface;
-    @Nullable
-    private float[] mTransform;
+
+    @NonNull
+    private final float[] mTransform = new float[16];
 
     /**
      * Creates a frame that is backed by a texture ID.
@@ -54,7 +57,7 @@
     /**
      * Checks if the frame is empty.
      *
-     * <p>A empty frame means that the texture does not have valid content. It can be filled
+     * <p>An empty frame means that the texture does not have valid content. It can be filled
      * with new content.
      */
     boolean isEmpty() {
@@ -69,25 +72,24 @@
     void markEmpty() {
         checkState(!isEmpty(), "Frame is already empty");
         mTimestampNs = NO_VALUE;
-        mTransform = null;
         mSurface = null;
     }
 
     /**
      * Marks the frame as filled.
      *
-     * <p>Call this method when a valid camera frame is copied to the texture with
+     * <p>Call this method when a valid camera frame has been copied to the texture with
      * {@link GlRenderer#renderInputToQueueTexture}. Once filled, the frame should not be
      * written into until it's made empty again.
      *
-     * @param timestampNs the timestamp of the camera frame.
+     * @param timestampNs the timestamp of the camera frame in nanoseconds.
      * @param transform   the transform to apply when rendering the frame.
      * @param surface     the output surface to which the frame should render.
      */
-    void markFilled(long timestampNs, float[] transform, Surface surface) {
+    void markFilled(long timestampNs, @NonNull float[] transform, @NonNull Surface surface) {
         checkState(isEmpty(), "Frame is already filled");
         mTimestampNs = timestampNs;
-        mTransform = transform;
+        System.arraycopy(transform, 0, mTransform, 0, transform.length);
         mSurface = surface;
     }
 
@@ -117,7 +119,7 @@
      */
     @NonNull
     float[] getTransform() {
-        return requireNonNull(mTransform);
+        return mTransform;
     }
 
     /**
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
index d68839d..a7760e2 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
@@ -20,11 +20,15 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.camera.effects.opengl.GlRenderer;
 
 /**
  * A buffer of {@link TextureFrame}.
+ *
+ * <p>This class is not thread safe. It is expected to be called from a single GL thread.
  */
+@RequiresApi(21)
 class TextureFrameBuffer {
 
     @NonNull
diff --git a/camera/camera-effects/src/test/java/androidx/camera/effects/internal/FrameBufferTest.kt b/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt
similarity index 84%
rename from camera/camera-effects/src/test/java/androidx/camera/effects/internal/FrameBufferTest.kt
rename to camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt
index 636c909..757373c 100644
--- a/camera/camera-effects/src/test/java/androidx/camera/effects/internal/FrameBufferTest.kt
+++ b/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt
@@ -35,7 +35,7 @@
 @RunWith(RobolectricTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class FrameBufferTest {
+class TextureFrameBufferTest {
 
     companion object {
         private const val TIMESTAMP_1 = 11L
@@ -82,8 +82,25 @@
     }
 
     @Test
+    fun getFrameToRender_returnsFilledFrame() {
+        // Arrange: create a buffer with 1 texture and mark them filled.
+        val buffer = TextureFrameBuffer(intArrayOf(1))
+        buffer.frameToFill.markFilled(TIMESTAMP_1, transform1, surface1)
+
+        // Act: get frame with the same timestamp.
+        val frame = buffer.getFrameToRender(TIMESTAMP_1)!!
+
+        // Assert: the frame has the correct values.
+        assertThat(frame.textureId).isEqualTo(1)
+        assertThat(frame.timestampNs).isEqualTo(TIMESTAMP_1)
+        assertThat(frame.transform.contentEquals(transform1)).isTrue()
+        assertThat(frame.transform).isNotSameInstanceAs(transform1)
+        assertThat(frame.surface).isSameInstanceAs(surface1)
+    }
+
+    @Test
     fun getFrameToRender_returnsNullIfNotFound() {
-        // Arrange: create a buffer with 1 texture and mark them occupied.
+        // Arrange: create a buffer with 1 texture and mark them filled.
         val buffer = TextureFrameBuffer(intArrayOf(1))
         buffer.frameToFill.markFilled(TIMESTAMP_1, transform1, surface1)
         // Act and assert: get frame with a different timestamp and it should be null.
@@ -91,15 +108,15 @@
     }
 
     @Test
-    fun getFrameToRender_markOlderFramesVacant() {
-        // Arrange: create a buffer with two textures and mark them occupied.
+    fun getFrameToRender_markOlderFramesEmpty() {
+        // Arrange: create a buffer with two textures and mark them filled.
         val buffer = TextureFrameBuffer(intArrayOf(1, 2))
         val frames = fillBufferWithTwoFrames(buffer)
 
         // Act: get frame2 for rendering.
         assertThat(buffer.getFrameToRender(TIMESTAMP_2)).isSameInstanceAs(frames.second)
 
-        // Assert: frame1 is vacant now.
+        // Assert: frame1 is empty now.
         assertThat(frames.second.isEmpty).isFalse()
         assertThat(frames.first.isEmpty).isTrue()
     }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 1b93298..d39f6dc 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -35,6 +35,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_FRAME_RATE;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_VIDEO_STABILIZATION_MODE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 import static androidx.camera.core.impl.utils.Threads.isMainThread;
 import static androidx.camera.core.impl.utils.TransformUtils.rectToString;
@@ -104,6 +105,7 @@
 import androidx.camera.core.impl.Timebase;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.TransformUtils;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
@@ -282,6 +284,14 @@
     }
 
     /**
+     * Returns whether video stabilization is enabled.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public boolean isVideoStabilizationEnabled() {
+        return getCurrentConfig().getVideoStabilizationMode() == StabilizationMode.ON;
+    }
+
+    /**
      * Sets the desired rotation of the output video.
      *
      * <p>Valid values include: {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
@@ -684,6 +694,7 @@
         // Use the frame rate range directly from the StreamSpec here (don't resolve it to the
         // default if unresolved).
         sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
+        sessionConfigBuilder.setVideoStabilization(config.getVideoStabilizationMode());
         sessionConfigBuilder.addErrorListener(
                 (sessionConfig, error) -> resetPipeline(cameraId, config, streamSpec));
         if (USE_TEMPLATE_PREVIEW_BY_QUIRK) {
@@ -1798,6 +1809,20 @@
             return this;
         }
 
+        /**
+         * Enable video stabilization.
+         *
+         * @param enabled True if enable, otherwise false.
+         * @return the current Builder.
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder<T> setVideoStabilizationEnabled(boolean enabled) {
+            getMutableConfig().insertOption(OPTION_VIDEO_STABILIZATION_MODE,
+                    enabled ? StabilizationMode.ON : StabilizationMode.OFF);
+            return this;
+        }
+
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         @Override
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 6f0e1a9..141fe23 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -1354,6 +1354,14 @@
         ).isEqualTo(newImplementationOptionValue)
     }
 
+    @Test
+    fun canSetVideoStabilization() {
+        val videoCapture = VideoCapture.Builder(Recorder.Builder().build())
+            .setVideoStabilizationEnabled(true)
+            .build()
+        assertThat(videoCapture.isVideoStabilizationEnabled).isTrue()
+    }
+
     private fun testSurfaceRequestContainsExpected(
         quality: Quality = HD, // HD maps to 1280x720 (4:3)
         videoEncoderInfo: VideoEncoderInfo = createVideoEncoderInfo(),
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
index e244909..d25cd94 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
@@ -177,7 +177,15 @@
     @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
     fun openCloseCameraStressTest_withPreviewVideoCaptureImageCapture(): Unit = runBlocking {
         val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
-        assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture, imageCapture))
+        // TODO(b/297311194): allow stream sharing once processing pipeline supports Camera2Interop
+        assumeTrue(
+            camera.isUseCasesCombinationSupported(
+                false,
+                preview,
+                videoCapture,
+                imageCapture
+            )
+        )
         bindUseCase_unbindAll_toCheckCameraState_repeatedly(
             preview,
             videoCapture = videoCapture,
@@ -192,7 +200,15 @@
     fun openCloseCameraStressTest_withPreviewVideoCaptureImageAnalysis(): Unit = runBlocking {
         val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
         val imageAnalysis = ImageAnalysis.Builder().build()
-        assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture, imageAnalysis))
+        // TODO(b/297311194): allow stream sharing once processing pipeline supports Camera2Interop
+        assumeTrue(
+            camera.isUseCasesCombinationSupported(
+                false,
+                preview,
+                videoCapture,
+                imageAnalysis
+            )
+        )
         bindUseCase_unbindAll_toCheckCameraState_repeatedly(
             preview,
             videoCapture = videoCapture,
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
index cd7c3c9..59c634b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
@@ -168,7 +168,16 @@
     fun openCloseCaptureSessionStressTest_withPreviewVideoCaptureImageCapture(): Unit =
         runBlocking {
             val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
-            assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture, imageCapture))
+            // TODO(b/297311194): allow stream sharing once processing pipeline supports
+            //  Camera2Interop
+            assumeTrue(
+                camera.isUseCasesCombinationSupported(
+                    false,
+                    preview,
+                    videoCapture,
+                    imageCapture
+                )
+            )
             bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
                 preview,
                 videoCapture = videoCapture,
@@ -183,7 +192,16 @@
         runBlocking {
             val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
             val imageAnalysis = ImageAnalysis.Builder().build()
-            assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture, imageAnalysis))
+            // TODO(b/297311194): allow stream sharing once processing pipeline supports
+            //  Camera2Interop
+            assumeTrue(
+                camera.isUseCasesCombinationSupported(
+                    false,
+                    preview,
+                    videoCapture,
+                    imageAnalysis
+                )
+            )
             bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
                 preview,
                 videoCapture = videoCapture,
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index c653b5f..f89839e 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -541,6 +541,7 @@
         return mPhotoToggle.isChecked() && cameraInfo != null && cameraInfo.hasFlashUnit();
     }
 
+    @SuppressLint("RestrictedApiAndroidX")
     private boolean isFlashTestSupported(@ImageCapture.FlashMode int flashMode) {
         switch (flashMode) {
             case FLASH_MODE_OFF:
@@ -548,7 +549,8 @@
             case FLASH_MODE_AUTO:
                 CameraInfo cameraInfo = getCameraInfo();
                 if (cameraInfo instanceof CameraInfoInternal) {
-                    Quirks deviceQuirks = DeviceQuirks.getAll();
+                    Quirks deviceQuirks =
+                            androidx.camera.camera2.internal.compat.quirk.DeviceQuirks.getAll();
                     Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();
                     if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)
                             || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index c2210f8..284cdd3 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -1464,7 +1464,9 @@
 package androidx.compose.foundation.text2 {
 
   public final class BasicSecureTextFieldKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text2.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,java.lang.Boolean>? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text2.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult>,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox, optional androidx.compose.foundation.ScrollState scrollState);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text2.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.text2.input.ImeActionHandler? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text2.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult>,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox, optional androidx.compose.foundation.ScrollState scrollState);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(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 androidx.compose.foundation.text2.input.ImeActionHandler? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text2.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.ScrollState scrollState, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult>,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.text2.input.ImeActionHandler? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text2.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.ScrollState scrollState, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult>,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
   }
 
   public final class BasicTextField2Kt {
@@ -1493,6 +1495,10 @@
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.foundation.text2.input.CodepointTransformation mask(androidx.compose.foundation.text2.input.CodepointTransformation.Companion, char character);
   }
 
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public fun interface ImeActionHandler {
+    method public boolean onImeAction(int action);
+  }
+
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public fun interface InputTransformation {
     method public default androidx.compose.foundation.text.KeyboardOptions? getKeyboardOptions();
     method public void transformInput(androidx.compose.foundation.text2.input.TextFieldCharSequence originalValue, androidx.compose.foundation.text2.input.TextFieldBuffer valueWithChanges);
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index a0aa8b2..1ab4f695 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -1466,7 +1466,9 @@
 package androidx.compose.foundation.text2 {
 
   public final class BasicSecureTextFieldKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text2.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,java.lang.Boolean>? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text2.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult>,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox, optional androidx.compose.foundation.ScrollState scrollState);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text2.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.text2.input.ImeActionHandler? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text2.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult>,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox, optional androidx.compose.foundation.ScrollState scrollState);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(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 androidx.compose.foundation.text2.input.ImeActionHandler? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text2.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.ScrollState scrollState, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult>,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.text2.input.ImeActionHandler? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text2.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.ScrollState scrollState, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult>,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
   }
 
   public final class BasicTextField2Kt {
@@ -1495,6 +1497,10 @@
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.foundation.text2.input.CodepointTransformation mask(androidx.compose.foundation.text2.input.CodepointTransformation.Companion, char character);
   }
 
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public fun interface ImeActionHandler {
+    method public boolean onImeAction(int action);
+  }
+
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public fun interface InputTransformation {
     method public default androidx.compose.foundation.text.KeyboardOptions? getKeyboardOptions();
     method public void transformInput(androidx.compose.foundation.text2.input.TextFieldCharSequence originalValue, androidx.compose.foundation.text2.input.TextFieldBuffer valueWithChanges);
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionLazy.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionLazy.kt
new file mode 100644
index 0000000..3e596cc
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionLazy.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.demos.text
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@Composable
+fun TextLazySelectionDemo() {
+    Column {
+        Text(
+            text = "We expect that selection works, regardless of how many times each text" +
+                " goes in or out of composition via scrolling the lazy column.",
+            modifier = Modifier.padding(16.dp),
+        )
+        SelectionContainer {
+            LazyColumn {
+                items(100) {
+                    BasicText(
+                        text = it.toString(),
+                        style = TextStyle(fontSize = fontSize8, textAlign = TextAlign.Center),
+                        modifier = Modifier.fillMaxWidth()
+                    )
+                }
+            }
+        }
+    }
+}
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 8b575e1..90304cc 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
@@ -152,6 +152,7 @@
                 ComposableDemo("Text selection") { TextSelectionDemo() },
                 ComposableDemo("Text selection sample") { TextSelectionSample() },
                 ComposableDemo("Overflowed Selection") { TextOverflowedSelectionDemo() },
+                ComposableDemo("LazyColumn Text Selection") { TextLazySelectionDemo() },
             )
         ),
         DemoCategory(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
index d3dd669..e6321c8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
@@ -275,6 +276,64 @@
     }
 
     @Test
+    fun swipeWithLowVelocity_onEdgeOfList_smallDeltas_shouldGoToClosestPage_backward() {
+        // Arrange
+        createPager(modifier = Modifier.fillMaxSize())
+        val delta = 10f * scrollForwardSign * -1
+
+        onPager().performTouchInput {
+            down(center)
+            // series of backward delta on edge
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+
+            // single delta on opposite direction
+            moveBy(
+                Offset(
+                    if (vertical) 0.0f else -delta,
+                    if (vertical) -delta else 0.0f
+                )
+            )
+            up()
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.isScrollInProgress == false }
+
+        // Assert
+        rule.onNodeWithTag("0").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(0)
+    }
+
+    @Test
+    fun swipeWithLowVelocity_onEdgeOfList_smallDeltas_shouldGoToClosestPage_forward() {
+        // Arrange
+        createPager(modifier = Modifier.fillMaxSize(), initialPage = DefaultPageCount - 1)
+        val delta = 10f * scrollForwardSign
+
+        onPager().performTouchInput {
+            down(center)
+            // series of backward delta on edge
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+            moveBy(Offset(if (vertical) 0.0f else delta, if (vertical) delta else 0.0f))
+
+            // single delta on opposite direction
+            moveBy(
+                Offset(
+                    if (vertical) 0.0f else -delta,
+                    if (vertical) -delta else 0.0f
+                )
+            )
+            up()
+        }
+        rule.mainClock.advanceTimeUntil { pagerState.isScrollInProgress == false }
+
+        // Assert
+        rule.onNodeWithTag("${DefaultPageCount - 1}").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(DefaultPageCount - 1)
+    }
+
+    @Test
     fun swipeWithLowVelocity_positionalThresholdOverThreshold_customPage_shouldGoToNextPage() {
         // Arrange
         createPager(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt
index 4d27892..f83cf84 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/FontScalingScreenshotTest.kt
@@ -37,6 +37,9 @@
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.LineHeightStyle.Alignment
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.em
 import androidx.compose.ui.unit.sp
@@ -94,6 +97,38 @@
     }
 
     @Test
+    fun fontScaling1x_lineHeightStyleDoubleSp() {
+        AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestLayout(
+                lineHeight = 28.sp,
+                lineHeightStyle = LineHeightStyle(Alignment.Bottom, Trim.Both)
+            )
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling1x_lineHeightStyleDoubleSp")
+    }
+
+    @Test
+    fun fontScaling2x_lineHeightStyleDoubleSp() {
+        AndroidFontScaleHelper.setSystemFontScale(2f, rule.activityRule.scenario)
+        rule.waitForIdle()
+
+        rule.setContent {
+            TestLayout(
+                lineHeight = 28.sp,
+                lineHeightStyle = LineHeightStyle(Alignment.Bottom, Trim.Both)
+            )
+        }
+        rule.onNodeWithTag(containerTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fontScaling2x_lineHeightStyleDoubleSp")
+    }
+
+    @Test
     fun fontScaling1x_lineHeightDoubleEm() {
         AndroidFontScaleHelper.setSystemFontScale(1f, rule.activityRule.scenario)
         rule.waitForIdle()
@@ -146,7 +181,10 @@
     }
 
     @Composable
-    private fun TestLayout(lineHeight: TextUnit) {
+    private fun TestLayout(
+        lineHeight: TextUnit,
+        lineHeightStyle: LineHeightStyle = LineHeightStyle.Default
+    ) {
         Column(
             modifier = Modifier.testTag(containerTag),
         ) {
@@ -177,7 +215,8 @@
                 style = TextStyle(
                     fontSize = 14.sp,
                     fontStyle = FontStyle.Italic,
-                    lineHeight = lineHeight
+                    lineHeight = lineHeight,
+                    lineHeightStyle = lineHeightStyle
                 )
             )
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt
index 8acdfa3..905a598 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/SelectionControllerTest.kt
@@ -27,6 +27,7 @@
 import androidx.compose.foundation.text.selection.Selection
 import androidx.compose.foundation.text.selection.Selection.AnchorInfo
 import androidx.compose.foundation.text.selection.SelectionAdjustment
+import androidx.compose.foundation.text.selection.SelectionLayoutBuilder
 import androidx.compose.foundation.text.selection.SelectionRegistrar
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
@@ -69,9 +70,11 @@
             it.addRect(Rect(0f, 0f, pathSize, pathSize))
         }
 
+        val fixedSelectionFake = FixedSelectionFake(0, 1000, 200)
         val subject = SelectionController(
-            FixedSelectionFake(0, 1000, 200),
-            Color.White,
+            selectableId = fixedSelectionFake.nextSelectableId(),
+            selectionRegistrar = fixedSelectionFake,
+            backgroundSelectionColor = Color.White,
             params = FakeParams(
                 path, true
             )
@@ -103,9 +106,11 @@
             it.addRect(Rect(0f, 0f, pathSize, pathSize))
         }
 
+        val fixedSelectionFake = FixedSelectionFake(0, 1000, 200)
         val subject = SelectionController(
-            FixedSelectionFake(0, 1000, 200),
-            Color.White,
+            selectableId = fixedSelectionFake.nextSelectableId(),
+            selectionRegistrar = fixedSelectionFake,
+            backgroundSelectionColor = Color.White,
             params = FakeParams(
                 path, false
             )
@@ -219,15 +224,7 @@
     override val selectableId: Long,
     private val lastVisible: Int
 ) : Selectable {
-    override fun updateSelection(
-        startHandlePosition: Offset,
-        endHandlePosition: Offset,
-        previousHandlePosition: Offset?,
-        isStartHandle: Boolean,
-        containerLayoutCoordinates: LayoutCoordinates,
-        adjustment: SelectionAdjustment,
-        previousSelection: Selection?
-    ): Pair<Selection?, Boolean> {
+    override fun appendSelectableInfoToBuilder(builder: SelectionLayoutBuilder) {
         FAKE()
     }
 
@@ -251,6 +248,18 @@
         FAKE()
     }
 
+    override fun getLineLeft(offset: Int): Float {
+        FAKE()
+    }
+
+    override fun getLineRight(offset: Int): Float {
+        FAKE()
+    }
+
+    override fun getCenterYForOffset(offset: Int): Float {
+        FAKE()
+    }
+
     override fun getRangeOfLineContaining(offset: Int): TextRange {
         FAKE()
     }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
index 854d909..43bbddd 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
@@ -17,13 +17,16 @@
 package androidx.compose.foundation.text.selection
 
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.text.Handle
+import androidx.compose.foundation.text.selection.gestures.util.longPress
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isUnspecified
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.getOrNull
@@ -40,8 +43,15 @@
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.lerp
 import androidx.test.filters.RequiresDevice
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import kotlin.math.sign
 import org.junit.Rule
 import org.junit.Test
@@ -57,7 +67,7 @@
     val rule = createComposeRule()
 
     protected val defaultMagnifierSize = IntSize.Zero
-    private val tag = "tag"
+    protected val tag = "tag"
 
     @Composable
     abstract fun TestContent(
@@ -86,6 +96,122 @@
         assertThat(center).isEqualTo(Offset.Unspecified)
     }
 
+    // Regression - magnifier should be constrained to end of line in BiDi,
+    // not the last offset which could be in middle of the line.
+    @Test
+    fun magnifier_centeredToEndOfLine_whenBidiEndOffsetInMiddleOfLine() {
+        val ltrWord = "hello"
+        val rtlWord = "בבבבב"
+
+        lateinit var textLayout: TextLayoutResult
+        rule.setContent {
+            Content(
+                text = """
+                    $rtlWord $ltrWord
+                    $ltrWord $rtlWord
+                    $rtlWord $ltrWord
+                """.trimIndent().trim(),
+                modifier = Modifier
+                    // Center the text to give the magnifier lots of room to move.
+                    .fillMaxSize()
+                    .wrapContentHeight()
+                    .testTag(tag),
+                onTextLayout = { textLayout = it }
+            )
+        }
+
+        val placedPosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
+
+        fun getCenterForLine(line: Int): Float {
+            val top = textLayout.getLineTop(line)
+            val bottom = textLayout.getLineBottom(line)
+            return (bottom - top) / 2 + top
+        }
+
+        val farRightX = rule.onNodeWithTag(tag).fetchSemanticsNode().boundsInRoot.right - 1f
+
+        rule.onNodeWithTag(tag).performTouchInput {
+            longPress(Offset(farRightX, getCenterForLine(0)))
+        }
+        rule.waitForIdle()
+        assertWithMessage("Magnifier should not be shown")
+            .that(getMagnifierCenterOffset(rule).isUnspecified)
+            .isTrue()
+
+        val secondLineCenterY = getCenterForLine(1)
+        val secondOffset = Offset(farRightX, secondLineCenterY)
+        rule.onNodeWithTag(tag).performTouchInput {
+            moveTo(secondOffset)
+        }
+        rule.waitForIdle()
+        assertWithMessage("Magnifier should not be shown")
+            .that(getMagnifierCenterOffset(rule).isUnspecified)
+            .isTrue()
+
+        val lineRightX = textLayout.getLineRight(1)
+        val thirdOffset = Offset(lineRightX + 1f, secondLineCenterY)
+        rule.onNodeWithTag(tag).performTouchInput {
+            moveTo(thirdOffset)
+        }
+        rule.waitForIdle()
+        val actual = getMagnifierCenterOffset(rule, requireSpecified = true) - placedPosition
+        assertThatOffset(actual).equalsWithTolerance(Offset(lineRightX, secondLineCenterY))
+    }
+
+    // regression - When dragging to the final empty line, the magnifier appeared on the second
+    // to last line instead of on the final line. It should appear on the final line.
+    @Test
+    fun magnifier_centeredOnCorrectLine_whenLinesAreEmpty() {
+        lateinit var textLayout: TextLayoutResult
+        rule.setContent {
+            Content(
+                "a\n\n",
+                Modifier
+                    // Center the text to give the magnifier lots of room to move.
+                    .fillMaxSize()
+                    .wrapContentSize()
+                    .testTag(tag),
+                onTextLayout = { textLayout = it }
+            )
+        }
+
+        rule.waitForIdle()
+        val placedOffset = rule.onNodeWithTag(tag).fetchSemanticsNode().boundsInRoot.topLeft
+        fun assertMagnifierAt(expected: Offset) {
+            rule.waitForIdle()
+            val actual = getMagnifierCenterOffset(rule, requireSpecified = true) - placedOffset
+            assertThatOffset(actual).equalsWithTolerance(expected)
+        }
+
+        // start selection at first character
+        val firstPressOffset = textLayout.getBoundingBox(0).centerLeft + Offset(1f, 0f)
+        rule.onNodeWithTag(tag).performTouchInput {
+            longPress(firstPressOffset)
+        }
+        assertMagnifierAt(firstPressOffset)
+
+        fun getOffsetAtLine(line: Int): Offset = Offset(
+            x = firstPressOffset.x,
+            y = lerp(
+                start = textLayout.getLineTop(lineIndex = line),
+                stop = textLayout.getLineBottom(lineIndex = line),
+                fraction = 0.5f
+            )
+        )
+
+        val secondOffset = getOffsetAtLine(1)
+        rule.onNodeWithTag(tag).performTouchInput {
+            moveTo(secondOffset)
+        }
+        assertMagnifierAt(Offset(0f, secondOffset.y))
+
+        val thirdOffset = getOffsetAtLine(2)
+        rule.onNodeWithTag(tag).performTouchInput {
+            moveTo(thirdOffset)
+        }
+        assertMagnifierAt(Offset(0f, thirdOffset.y))
+    }
+
     @Test
     fun magnifier_hidden_whenTextIsEmpty() {
         rule.setContent {
@@ -430,25 +556,15 @@
         // Touch and move the handle to show the magnifier.
         rule.onNode(isSelectionHandle(handle)).performTouchInput {
             down(center)
-            // If cursor, we have to drag the cursor to show the magnifier,
-            // press alone will not suffice
-            if (handle == Handle.Cursor) {
-                movePastSlopBy(moveOffset)
-            }
+            movePastSlopBy(moveOffset)
         }
 
         val magnifierInitialPosition = getMagnifierCenterOffset(rule, requireSpecified = true)
 
         // Drag just a little past the end of the line.
-        rule.onNode(isSelectionHandle(handle))
-            .performTouchInput {
-                if (handle == Handle.Cursor) {
-                    // If cursor, we dragged past slop before, so just move the normal delta
-                    moveBy(moveOffset)
-                } else {
-                    movePastSlopBy(moveOffset)
-                }
-            }
+        rule.onNode(isSelectionHandle(handle)).performTouchInput {
+            moveBy(moveOffset)
+        }
 
         // The magnifier shouldn't have moved.
         assertThat(getMagnifierCenterOffset(rule)).isEqualTo(magnifierInitialPosition)
@@ -615,3 +731,30 @@
         moveBy(delta + slop)
     }
 }
+
+internal fun assertThatOffset(actual: Offset): OffsetSubject =
+    Truth.assertAbout(OffsetSubject.INSTANCE).that(actual)
+
+internal class OffsetSubject(
+    failureMetadata: FailureMetadata?,
+    private val subject: Offset,
+) : Subject(failureMetadata, subject) {
+
+    companion object {
+        val INSTANCE: Factory<OffsetSubject, Offset> =
+            Factory { failureMetadata, subject -> OffsetSubject(failureMetadata, subject) }
+    }
+
+    fun equalsWithTolerance(expected: Offset, tolerance: Float = 0.001f) {
+        try {
+            assertThat(subject.x).isWithin(tolerance).of(expected.x)
+            assertThat(subject.y).isWithin(tolerance).of(expected.y)
+        } catch (e: AssertionError) {
+            failWithActual(
+                Fact.simpleFact("Unequal Offsets"),
+                Fact.fact("expected", expected.toString()),
+                Fact.fact("with tolerance", tolerance),
+            )
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
index 10f9af8..65d6fb3 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
@@ -1860,1524 +1860,6 @@
         }
     }
 
-    @Test
-    fun getTextSelectionInfo_long_press_select_word_ltr() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        val start = Offset((fontSizeInPx * 2), (fontSizeInPx / 2))
-        val end = start
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = end,
-            endHandlePosition = start,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(0)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo("hello".length)
-        }
-    }
-
-    @Test
-    fun getTextSelectionInfo_long_press_select_word_rtl() {
-        val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        val start = Offset((fontSizeInPx * 2), (fontSizeInPx / 2))
-        val end = start
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = end,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(text.indexOf("\u05D3"))
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(text.indexOf("\u05D5") + 1)
-        }
-    }
-
-    @Test
-    fun getTextSelectionInfo_long_press_drag_handle_not_cross_select_word() {
-        val text = "hello world"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        val rawStartOffset = text.indexOf('e')
-        val rawEndOffset = text.indexOf('r')
-        val start = Offset((fontSizeInPx * rawStartOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * rawEndOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(0)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(text.length)
-        }
-        assertThat(textSelectionInfo?.handlesCrossed).isFalse()
-    }
-
-    @Test
-    fun getTextSelectionInfo_long_press_drag_handle_cross_select_word() {
-        val text = "hello world"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        val rawStartOffset = text.indexOf('r')
-        val rawEndOffset = text.indexOf('e')
-        val start = Offset((fontSizeInPx * rawStartOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * rawEndOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(text.length)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(0)
-        }
-        assertThat(textSelectionInfo?.handlesCrossed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_long_press_select_ltr_drag_down() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // long pressed between "h" and "e", "hello" should be selected
-        val start = Offset((fontSizeInPx * 2), (fontSizeInPx / 2))
-        val end = start
-
-        // Act.
-        val (textSelectionInfo1, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Drag downwards, after the drag the selection should remain the same.
-        val (textSelectionInfo2, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = end + Offset(0f, fontSizeInPx / 4),
-            endHandlePosition = start,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Word,
-            previousSelection = textSelectionInfo1,
-            isStartHandle = false
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo1).isNotNull()
-
-        assertThat(textSelectionInfo1?.start).isNotNull()
-        textSelectionInfo1?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(0)
-        }
-
-        assertThat(textSelectionInfo1?.end).isNotNull()
-        textSelectionInfo1?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo("hello".length)
-        }
-
-        assertThat(textSelectionInfo2).isNotNull()
-        assertThat(textSelectionInfo2).isEqualTo(textSelectionInfo1)
-    }
-
-    @Test
-    fun getTextSelectionInfo_drag_select_range_ltr() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "llo wor" is selected.
-        val startOffset = text.indexOf("l")
-        val endOffset = text.indexOf("r") + 1
-        val start = Offset((fontSizeInPx * startOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * endOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(startOffset)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(endOffset)
-        }
-    }
-
-    @Test
-    fun getTextSelectionInfo_drag_select_range_rtl() {
-        val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "\u05D1\u05D2 \u05D3" is selected.
-        val startOffset = text.indexOf("\u05D1")
-        val endOffset = text.indexOf("\u05D3") + 1
-        val start = Offset(
-            (fontSizeInPx * (text.length - 1 - startOffset)),
-            (fontSizeInPx / 2)
-        )
-        val end = Offset(
-            (fontSizeInPx * (text.length - 1 - endOffset)),
-            (fontSizeInPx / 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(startOffset)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(endOffset)
-        }
-    }
-
-    @Test
-    fun getTextSelectionInfo_drag_select_range_bidi() {
-        val textLtr = "Hello"
-        val textRtl = "\u05D0\u05D1\u05D2\u05D3\u05D4"
-        val text = textLtr + textRtl
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "llo"+"\u05D0\u05D1\u05D2" is selected
-        val startOffset = text.indexOf("l")
-        val endOffset = text.indexOf("\u05D2") + 1
-        val start = Offset(
-            (fontSizeInPx * startOffset),
-            (fontSizeInPx / 2)
-        )
-        val end = Offset(
-            (fontSizeInPx * (textLtr.length + text.length - endOffset)),
-            (fontSizeInPx / 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(startOffset)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(endOffset)
-        }
-    }
-
-    @Test
-    fun getTextSelectionInfo_single_widget_handles_crossed_ltr() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "llo wor" is selected.
-        val startOffset = text.indexOf("r") + 1
-        val endOffset = text.indexOf("l")
-        val start = Offset((fontSizeInPx * startOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * endOffset), (fontSizeInPx / 2))
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(startOffset)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(endOffset)
-        }
-        assertThat(textSelectionInfo?.handlesCrossed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_single_widget_handles_crossed_rtl() {
-        val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "\u05D1\u05D2 \u05D3" is selected.
-        val startOffset = text.indexOf("\u05D3") + 1
-        val endOffset = text.indexOf("\u05D1")
-        val start = Offset(
-            (fontSizeInPx * (text.length - 1 - startOffset)),
-            (fontSizeInPx / 2)
-        )
-        val end = Offset(
-            (fontSizeInPx * (text.length - 1 - endOffset)),
-            (fontSizeInPx / 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(startOffset)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(endOffset)
-        }
-        assertThat(textSelectionInfo?.handlesCrossed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_single_widget_handles_crossed_bidi() {
-        val textLtr = "Hello"
-        val textRtl = "\u05D0\u05D1\u05D2\u05D3\u05D4"
-        val text = textLtr + textRtl
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "llo"+"\u05D0\u05D1\u05D2" is selected
-        val startOffset = text.indexOf("\u05D2") + 1
-        val endOffset = text.indexOf("l")
-        val start = Offset(
-            (fontSizeInPx * (textLtr.length + text.length - startOffset)),
-            (fontSizeInPx / 2)
-        )
-        val end = Offset(
-            (fontSizeInPx * endOffset),
-            (fontSizeInPx / 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(startOffset)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(endOffset)
-        }
-        assertThat(textSelectionInfo?.handlesCrossed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_ltr_drag_endHandle() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "llo" is selected.
-        val oldStartOffset = text.indexOf("l")
-        val oldEndOffset = text.indexOf("o") + 1
-        val selectableId = 1L
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-        // first "l" is selected.
-        val start = Offset((fontSizeInPx * oldStartOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldStartOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = false
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isEqualTo(previousSelection.start)
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(oldStartOffset + 1)
-        }
-
-        assertThat(textSelectionInfo?.handlesCrossed).isFalse()
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_rtl_drag_endHandle() {
-        val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "\u05D0\u05D1" is selected.
-        val oldStartOffset = text.indexOf("\u05D1")
-        val oldEndOffset = text.length
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Rtl,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Rtl,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-        // "\u05D1" is selected.
-        val start = Offset(
-            (fontSizeInPx * (text.length - 1 - oldStartOffset)),
-            (fontSizeInPx / 2)
-        )
-        val end = Offset(
-            (fontSizeInPx * (text.length - 1 - oldStartOffset)),
-            (fontSizeInPx / 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = false
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isEqualTo(previousSelection.start)
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Rtl)
-            assertThat(it.offset).isEqualTo(oldStartOffset + 1)
-        }
-
-        assertThat(textSelectionInfo?.handlesCrossed).isFalse()
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_startHandle_not_crossed() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "llo" is selected.
-        val oldStartOffset = text.indexOf("l")
-        val oldEndOffset = text.indexOf("o") + 1
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-        // "o" is selected.
-        val start = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = true
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo((oldEndOffset - 1))
-        }
-
-        assertThat(textSelectionInfo?.end).isEqualTo(previousSelection.end)
-
-        assertThat(textSelectionInfo?.handlesCrossed).isFalse()
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_startHandle_crossed() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "llo" is selected.
-        val oldStartOffset = text.indexOf("o") + 1
-        val oldEndOffset = text.indexOf("l")
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-        // first "l" is selected.
-        val start = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = true
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo((oldEndOffset + 1))
-        }
-
-        assertThat(textSelectionInfo?.end).isEqualTo(previousSelection.end)
-
-        assertThat(textSelectionInfo?.handlesCrossed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_startHandle_not_crossed_bounded() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "e" is selected.
-        val oldStartOffset = text.indexOf("e")
-        val oldEndOffset = text.indexOf("l")
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-        // "e" should be selected.
-        val start = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = true
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo?.start?.offset).isEqualTo(previousSelection.start.offset)
-        assertThat(textSelectionInfo?.end?.offset).isEqualTo(previousSelection.end.offset)
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_startHandle_crossed_bounded() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "e" is selected.
-        val oldStartOffset = text.indexOf("l")
-        val oldEndOffset = text.indexOf("e")
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-        // "e" should be selected.
-        val start = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = true
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo?.start?.offset).isEqualTo(previousSelection.start.offset)
-        assertThat(textSelectionInfo?.end?.offset).isEqualTo(previousSelection.end.offset)
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_startHandle_not_crossed_boundary() {
-        val text = "hello world"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "d" is selected.
-        val oldStartOffset = text.length - 1
-        val oldEndOffset = text.length
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-        // "d" should be selected.
-        val start = Offset(
-            (fontSizeInPx * oldEndOffset) - (fontSizeInPx / 2),
-            (fontSizeInPx / 2)
-        )
-        val end = Offset(
-            (fontSizeInPx * oldEndOffset) - 1,
-            (fontSizeInPx / 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = true
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo?.start?.offset).isEqualTo(previousSelection.start.offset)
-        assertThat(textSelectionInfo?.end?.offset).isEqualTo(previousSelection.end.offset)
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_startHandle_crossed_boundary() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "h" is selected.
-        val oldStartOffset = text.indexOf("e")
-        val oldEndOffset = 0
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-        // "e" should be selected.
-        val start = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldEndOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = true
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo?.start?.offset).isEqualTo(previousSelection.start.offset)
-        assertThat(textSelectionInfo?.end?.offset).isEqualTo(previousSelection.end.offset)
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_endHandle_crossed() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "llo" is selected.
-        val oldStartOffset = text.indexOf("o") + 1
-        val oldEndOffset = text.indexOf("l")
-        val selectableId = 1L
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-        // "o" is selected.
-        val start = Offset((fontSizeInPx * oldStartOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldStartOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = false
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isEqualTo(previousSelection.start)
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo((oldStartOffset - 1))
-        }
-
-        assertThat(textSelectionInfo?.handlesCrossed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_endHandle_not_crossed_bounded() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "e" is selected.
-        val oldStartOffset = text.indexOf("e")
-        val oldEndOffset = text.indexOf("l")
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-        // "e" should be selected.
-        val start = Offset((fontSizeInPx * oldStartOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldStartOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = false
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo?.start?.offset).isEqualTo(previousSelection.start.offset)
-        assertThat(textSelectionInfo?.end?.offset).isEqualTo(previousSelection.end.offset)
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_endHandle_crossed_bounded() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "e" is selected.
-        val oldStartOffset = text.indexOf("l")
-        val oldEndOffset = text.indexOf("e")
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-        // "e" should be selected.
-        val start = Offset((fontSizeInPx * oldStartOffset), (fontSizeInPx / 2))
-        val end = Offset((fontSizeInPx * oldStartOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = false
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo?.start?.offset).isEqualTo(previousSelection.start.offset)
-        assertThat(textSelectionInfo?.end?.offset).isEqualTo(previousSelection.end.offset)
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_endHandle_not_crossed_boundary() {
-        val text = "hello world"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "h" is selected.
-        val oldStartOffset = 0
-        val oldEndOffset = text.indexOf('e')
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = false
-        )
-        // "h" should be selected.
-        val start = Offset(
-            (fontSizeInPx * oldStartOffset),
-            (fontSizeInPx / 2)
-        )
-        val end = Offset(
-            (fontSizeInPx * oldStartOffset),
-            (fontSizeInPx / 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = false
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo?.start?.offset).isEqualTo(previousSelection.start.offset)
-        assertThat(textSelectionInfo?.end?.offset).isEqualTo(previousSelection.end.offset)
-    }
-
-    @Test
-    fun getTextSelectionInfo_bound_to_one_character_drag_endHandle_crossed_boundary() {
-        val text = "hello world"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "d" is selected.
-        val oldStartOffset = text.length
-        val oldEndOffset = text.length - 1
-        val selectableId = 1L
-
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                offset = oldStartOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            end = Selection.AnchorInfo(
-                offset = oldEndOffset,
-                direction = ResolvedTextDirection.Ltr,
-                selectableId = selectableId
-            ),
-            handlesCrossed = true
-        )
-        // "d" should be selected.
-        val start = Offset(
-            (fontSizeInPx * oldStartOffset) - 1,
-            (fontSizeInPx / 2)
-        )
-        val end = Offset(
-            (fontSizeInPx * oldStartOffset) - 1,
-            (fontSizeInPx / 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Character,
-            previousSelection = previousSelection,
-            isStartHandle = false
-        )
-
-        // Assert.
-        assertThat(textSelectionInfo?.start?.offset).isEqualTo(previousSelection.start.offset)
-        assertThat(textSelectionInfo?.end?.offset).isEqualTo(previousSelection.end.offset)
-    }
-
-    @Test
-    fun getTextSelectionInfo_cross_widget_not_contain_start() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "hello w" is selected.
-        val endOffset = text.indexOf("w") + 1
-        val start = Offset(-50f, -50f)
-        val end = Offset((fontSizeInPx * endOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(0)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(endOffset)
-        }
-    }
-
-    @Test
-    fun getTextSelectionInfo_cross_widget_not_contain_end() {
-        val text = "hello world"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "o world" is selected.
-        val startOffset = text.indexOf("o")
-        val start = Offset((fontSizeInPx * startOffset), (fontSizeInPx / 2))
-        val end = Offset(
-            (fontSizeInPx * text.length * 2), (fontSizeInPx * 2)
-        )
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(startOffset)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(text.length)
-        }
-    }
-
-    @Test
-    fun getTextSelectionInfo_cross_widget_not_contain_start_handles_crossed() {
-        val text = "hello world"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "world" is selected.
-        val endOffset = text.indexOf("w")
-        val start =
-            Offset((fontSizeInPx * text.length * 2), (fontSizeInPx * 2))
-        val end = Offset((fontSizeInPx * endOffset), (fontSizeInPx / 2))
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(text.length)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(endOffset)
-        }
-        assertThat(textSelectionInfo?.handlesCrossed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_cross_widget_not_contain_end_handles_crossed() {
-        val text = "hello world"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        // "hell" is selected.
-        val startOffset = text.indexOf("o")
-        val start =
-            Offset((fontSizeInPx * startOffset), (fontSizeInPx / 2))
-        val end = Offset(-50f, -50f)
-
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.None
-        )
-        // Assert.
-        assertThat(textSelectionInfo).isNotNull()
-
-        assertThat(textSelectionInfo?.start).isNotNull()
-        textSelectionInfo?.start?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(startOffset)
-        }
-
-        assertThat(textSelectionInfo?.end).isNotNull()
-        textSelectionInfo?.end?.let {
-            assertThat(it.direction).isEqualTo(ResolvedTextDirection.Ltr)
-            assertThat(it.offset).isEqualTo(0)
-        }
-        assertThat(textSelectionInfo?.handlesCrossed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_not_selected() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        val start = Offset(-50f, -50f)
-        val end = Offset(-20f, -20f)
-        // Act.
-        val (textSelectionInfo, _) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = null,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Word
-        )
-        assertThat(textSelectionInfo).isNull()
-    }
-
-    @Test
-    fun getTextSelectionInfo_handleNotMoved_selectionUpdated_consumed_isTrue() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        val start = Offset(0f, fontSizeInPx / 2)
-        val end = Offset(fontSizeInPx * 3, fontSizeInPx / 2)
-        // Act.
-        // Selection is updated but endHandlePosition is actually the same. Since selection is
-        // updated, the movement is consumed.
-        val (_, consumed) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = end,
-            selectableId = 1,
-            adjustment = SelectionAdjustment.Word,
-            previousSelection = null,
-            isStartHandle = false
-        )
-        assertThat(consumed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_handleMoved_selectionNotUpdated_consumed_isTrue() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        val start = Offset(0f, fontSizeInPx / 2)
-        val end = Offset(fontSizeInPx * 3, fontSizeInPx / 2)
-        val previousEnd = Offset(fontSizeInPx * 2, fontSizeInPx / 2)
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = 0,
-                selectableId = 1
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = 5,
-                selectableId = 1
-            ),
-        )
-        // Act.
-        // End handle moved from offset 2 to 3. But we are using word based selection, so the
-        // selection is still [0, 5). However, since handle moved to a new offset, the movement
-        // is consumed.
-        val (textSelectionInfo, consumed) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = previousEnd,
-            selectableId = 1,
-            previousSelection = previousSelection,
-            adjustment = SelectionAdjustment.Word,
-            isStartHandle = false
-        )
-        // First check that Selection didn't update.
-        assertThat(textSelectionInfo).isEqualTo(previousSelection)
-        assertThat(consumed).isTrue()
-    }
-
-    @Test
-    fun getTextSelectionInfo_handleNotMoved_selectionNotUpdated_consumed_isFalse() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-        val start = Offset(0f, fontSizeInPx / 2)
-        val end = Offset(fontSizeInPx * 2.8f, fontSizeInPx / 2)
-        val previousEnd = Offset(fontSizeInPx * 3f, fontSizeInPx / 2)
-        val previousSelection = Selection(
-            start = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = 0,
-                selectableId = 1
-            ),
-            end = Selection.AnchorInfo(
-                direction = ResolvedTextDirection.Ltr,
-                offset = 5,
-                selectableId = 1
-            ),
-        )
-        // Act.
-        // End handle moved, but is still at the offset 3. Selection is not updated either.
-        // So the movement is not consumed.
-        val (textSelectionInfo, consumed) = getTextSelectionInfo(
-            textLayoutResult = textLayoutResult,
-            startHandlePosition = start,
-            endHandlePosition = end,
-            previousHandlePosition = previousEnd,
-            selectableId = 1,
-            previousSelection = previousSelection,
-            adjustment = SelectionAdjustment.Word,
-            isStartHandle = false
-        )
-        // First check that Selection didn't update.
-        assertThat(textSelectionInfo).isEqualTo(previousSelection)
-        assertThat(consumed).isFalse()
-    }
-
     @OptIn(InternalFoundationTextApi::class)
     private fun simpleTextLayout(
         text: String = "",
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
index 00dba57..4735a9b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
@@ -16,12 +16,26 @@
 
 package androidx.compose.foundation.text.selection
 
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.Handle
+import androidx.compose.foundation.text.selection.gestures.util.longPress
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.RequiresDevice
@@ -58,4 +72,66 @@
     fun magnifier_hidden_whenSelectionStartDraggedBelowTextBounds_whenTextOverflowed() {
         checkMagnifierAsHandleGoesOutOfBoundsUsingMaxLines(Handle.SelectionStart)
     }
+
+    // Regression - magnifier on an empty RTL Text should appear on right side, not left
+    @Test
+    fun magnifier_whenRtlLayoutWithEmptyLine_positionedOnRightSide() {
+        val nonEmptyTag = "nonEmpty"
+        val emptyTag = "empty"
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                SelectionContainer(
+                    modifier = Modifier
+                        // Center the text to give the magnifier lots of room to move.
+                        .fillMaxSize()
+                        .wrapContentSize()
+                        .testTag(tag),
+                ) {
+                    Column(Modifier.width(IntrinsicSize.Max)) {
+                        BasicText(
+                            text = "בבבב",
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .testTag(nonEmptyTag)
+                        )
+                        BasicText(
+                            text = "",
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .testTag(emptyTag)
+                        )
+                    }
+                }
+            }
+        }
+
+        val placedPosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
+        fun assertMagnifierAt(expected: Offset) {
+            val actual = getMagnifierCenterOffset(rule, requireSpecified = true) - placedPosition
+            assertThatOffset(actual).equalsWithTolerance(expected)
+        }
+
+        // start selection at first character
+        val firstPressOffset = rule.onNodeWithTag(nonEmptyTag).fetchTextLayoutResult()
+            .getBoundingBox(0).centerRight - Offset(1f, 0f)
+
+        rule.onNodeWithTag(tag).performTouchInput {
+            longPress(firstPressOffset)
+        }
+        rule.waitForIdle()
+        assertMagnifierAt(firstPressOffset)
+
+        val emptyTextPosition = rule.onNodeWithTag(emptyTag).fetchSemanticsNode().positionInRoot
+        val textLayoutResult = rule.onNodeWithTag(emptyTag).fetchTextLayoutResult()
+        val emptyTextCenterY =
+            textLayoutResult.size.height / 2f + emptyTextPosition.y - placedPosition.y
+        val secondOffset = Offset(firstPressOffset.x, emptyTextCenterY)
+        rule.onNodeWithTag(tag).performTouchInput {
+            moveTo(secondOffset)
+        }
+        rule.waitForIdle()
+
+        val expectedX = rule.onNodeWithTag(tag).fetchSemanticsNode().boundsInRoot.width
+        assertMagnifierAt(Offset(expectedX, emptyTextCenterY))
+    }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionDelegateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionDelegateTest.kt
deleted file mode 100644
index f109ae9..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionDelegateTest.kt
+++ /dev/null
@@ -1,401 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.selection
-
-import androidx.activity.ComponentActivity
-import androidx.compose.foundation.text.InternalFoundationTextApi
-import androidx.compose.foundation.text.TEST_FONT_FAMILY
-import androidx.compose.foundation.text.TextDelegate
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.ExperimentalTextApi
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.createFontFamilyResolver
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.sp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class TextFieldSelectionDelegateTest {
-    @get:Rule
-    val composeTestRule = createAndroidComposeRule<ComponentActivity>()
-
-    private val fontFamily = TEST_FONT_FAMILY
-    private val context = InstrumentationRegistry.getInstrumentation().context
-    private val defaultDensity = Density(density = 1f)
-    @OptIn(ExperimentalTextApi::class)
-    private val fontFamilyResolver = createFontFamilyResolver(context)
-
-    @Test
-    fun getTextFieldSelection_long_press_select_word_ltr() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = 2,
-            rawEndOffset = 2,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(0)
-        assertThat(range.end).isEqualTo("hello".length)
-    }
-
-    @Test
-    fun getTextFieldSelection_long_press_select_word_rtl() {
-        val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = 5,
-            rawEndOffset = 5,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(text.indexOf("\u05D3"))
-        assertThat(range.end).isEqualTo(text.indexOf("\u05D5") + 1)
-    }
-
-    @Test
-    fun getTextFieldSelection_long_press_drag_handle_not_cross_select_word() {
-        val text = "hello world"
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        val rawStartOffset = text.indexOf('e')
-        val rawEndOffset = text.indexOf('r')
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = rawStartOffset,
-            rawEndOffset = rawEndOffset,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(0)
-        assertThat(range.end).isEqualTo(text.length)
-    }
-
-    @Test
-    fun getTextFieldSelection_long_press_drag_handle_cross_select_word() {
-        val text = "hello world"
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        val rawStartOffset = text.indexOf('r')
-        val rawEndOffset = text.indexOf('e')
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = rawStartOffset,
-            rawEndOffset = rawEndOffset,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(text.length)
-        assertThat(range.end).isEqualTo(0)
-    }
-
-    @Test
-    fun getTextFieldSelection_drag_select_range_ltr() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "llo wor" is selected.
-        val startOffset = text.indexOf("l")
-        val endOffset = text.indexOf("r") + 1
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = startOffset,
-            rawEndOffset = endOffset,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.None
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(startOffset)
-        assertThat(range.end).isEqualTo(endOffset)
-    }
-
-    @Test
-    fun getTextFieldSelection_drag_select_range_rtl() {
-        val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "\u05D1\u05D2 \u05D3" is selected.
-        val startOffset = text.indexOf("\u05D1")
-        val endOffset = text.indexOf("\u05D3") + 1
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = startOffset,
-            rawEndOffset = endOffset,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Character
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(startOffset)
-        assertThat(range.end).isEqualTo(endOffset)
-    }
-
-    @Test
-    fun getTextFieldSelection_drag_select_range_bidi() {
-        val textLtr = "Hello"
-        val textRtl = "\u05D0\u05D1\u05D2\u05D3\u05D4"
-        val text = textLtr + textRtl
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "llo"+"\u05D0\u05D1\u05D2" is selected
-        val startOffset = text.indexOf("l")
-        val endOffset = text.indexOf("\u05D2") + 1
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = startOffset,
-            rawEndOffset = endOffset,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Character
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(startOffset)
-        assertThat(range.end).isEqualTo(endOffset)
-    }
-
-    @Test
-    fun getTextFieldSelection_drag_handles_crossed_ltr() {
-        val text = "hello world\n"
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "llo wor" is selected.
-        val startOffset = text.indexOf("r") + 1
-        val endOffset = text.indexOf("l")
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = startOffset,
-            rawEndOffset = endOffset,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Character
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(startOffset)
-        assertThat(range.end).isEqualTo(endOffset)
-    }
-
-    @Test
-    fun getTextFieldSelection_drag_handles_crossed_rtl() {
-        val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "\u05D1\u05D2 \u05D3" is selected.
-        val startOffset = text.indexOf("\u05D3") + 1
-        val endOffset = text.indexOf("\u05D1")
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = startOffset,
-            rawEndOffset = endOffset,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Character
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(startOffset)
-        assertThat(range.end).isEqualTo(endOffset)
-    }
-
-    @Test
-    fun getTextFieldSelection_drag_handles_crossed_bidi() {
-        val textLtr = "Hello"
-        val textRtl = "\u05D0\u05D1\u05D2\u05D3\u05D4"
-        val text = textLtr + textRtl
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // "llo"+"\u05D0\u05D1\u05D2" is selected
-        val startOffset = text.indexOf("\u05D2") + 1
-        val endOffset = text.indexOf("l")
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = startOffset,
-            rawEndOffset = endOffset,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Character,
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(startOffset)
-        assertThat(range.end).isEqualTo(endOffset)
-    }
-
-    @Test
-    fun getTextFieldSelection_empty_string() {
-        val text = ""
-        val fontSize = 20.sp
-
-        val textLayoutResult = simpleTextLayout(
-            text = text,
-            fontSize = fontSize,
-            density = defaultDensity
-        )
-
-        // Act.
-        val range = getTextFieldSelection(
-            textLayoutResult = textLayoutResult,
-            rawStartOffset = 0,
-            rawEndOffset = 0,
-            previousHandleOffset = -1,
-            previousSelection = null,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.Word
-        )
-
-        // Assert.
-        assertThat(range.start).isEqualTo(0)
-        assertThat(range.end).isEqualTo(0)
-    }
-
-    @OptIn(InternalFoundationTextApi::class)
-    private fun simpleTextLayout(
-        text: String = "",
-        fontSize: TextUnit = TextUnit.Unspecified,
-        density: Density
-    ): TextLayoutResult {
-        val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
-        val annotatedString = AnnotatedString(text, spanStyle)
-        return TextDelegate(
-            text = annotatedString,
-            style = TextStyle(),
-            density = density,
-            fontFamilyResolver = fontFamilyResolver
-        ).layout(Constraints(), LayoutDirection.Ltr)
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/AbstractSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/AbstractSelectionGesturesTest.kt
index 83854f82..302cda7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/AbstractSelectionGesturesTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/AbstractSelectionGesturesTest.kt
@@ -16,13 +16,19 @@
 
 package androidx.compose.foundation.text.selection.gestures
 
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.foundation.text.selection.gestures.util.FakeHapticFeedback
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.testutils.TestViewConfiguration
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.geometry.toRect
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.LocalTextToolbar
@@ -31,16 +37,19 @@
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.MouseInjectionScope
 import androidx.compose.ui.test.TouchInjectionScope
-import androidx.compose.ui.test.getBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipe
+import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.compose.ui.unit.toSize
 import androidx.compose.ui.util.lerp
+import java.lang.AssertionError
 import kotlin.math.max
 import kotlin.math.roundToInt
 import org.junit.Before
@@ -54,8 +63,11 @@
 
     protected abstract val pointerAreaTag: String
 
+    protected open var textDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr
+
     protected val hapticFeedback = FakeHapticFeedback()
     protected val fontFamily = TEST_FONT_FAMILY
+
     // small enough to fit in narrow screen in pre-submit,
     // big enough that pointer movement can target a single char on center
     protected val fontSize = 15.sp
@@ -77,31 +89,87 @@
                 ),
                 LocalHapticFeedback provides hapticFeedback,
             ) {
-                Content()
+                Box(
+                    modifier = Modifier
+                        .padding(32.dp)
+                        .fillMaxSize()
+                        .wrapContentSize()
+                ) {
+                    Content()
+                }
             }
         }
     }
 
-    protected val boundsInRoot
-        get() = with(density) {
-            rule.onNodeWithTag(pointerAreaTag).getBoundsInRoot().toRect()
+    private val bounds
+        get() = rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().size.toSize().toRect()
+
+    private val left get() = bounds.left + 1f
+    private val top get() = bounds.top + 1f
+    private val right get() = bounds.right - 1f
+    private val bottom get() = bounds.bottom - 1f
+
+    private val start get() = textDirection.take(ltr = left, rtl = right)
+    private val end get() = textDirection.take(ltr = right, rtl = left)
+
+    protected val topStart get() = Offset(start, top)
+    protected val centerStart get() = Offset(start, center.y)
+    protected val bottomStart get() = Offset(start, bottom)
+
+    protected val center get() = bounds.center
+
+    protected val topEnd get() = Offset(end, top)
+    protected val centerEnd get() = Offset(end, center.y)
+    protected val bottomEnd get() = Offset(end, bottom)
+
+    protected enum class VerticalDirection { UP, DOWN, CENTER }
+    protected enum class HorizontalDirection { START, END, CENTER }
+
+    // nudge 2f since we start 1f inwards from the edges and want to ensure we move over them if
+    // we nudge outwards again
+    protected fun Offset.nudge(
+        xDirection: HorizontalDirection = HorizontalDirection.CENTER,
+        yDirection: VerticalDirection = VerticalDirection.CENTER,
+    ): Offset = Offset(
+        x = x.adjustHorizontal(xDirection, 2f),
+        y = y.adjustVertical(yDirection, 2f),
+    )
+
+    private fun Float.adjustVertical(direction: VerticalDirection, diff: Float): Float =
+        this + diff * when (direction) {
+            VerticalDirection.UP -> -1f
+            VerticalDirection.CENTER -> 0f
+            VerticalDirection.DOWN -> 1f
         }
 
+    private fun Float.adjustHorizontal(direction: HorizontalDirection, diff: Float): Float =
+        this + diff * when (direction) {
+            HorizontalDirection.START -> textDirection.take(ltr = -1f, rtl = 1f)
+            HorizontalDirection.CENTER -> 0f
+            HorizontalDirection.END -> textDirection.take(ltr = 1f, rtl = -1f)
+        }
+
+    private fun <T> ResolvedTextDirection.take(ltr: T, rtl: T): T = when (this) {
+        ResolvedTextDirection.Ltr -> ltr
+        ResolvedTextDirection.Rtl -> rtl
+        else -> throw AssertionError("Unrecognized text direction $textDirection")
+    }
+
     // TODO(b/281584353) When touch mode can be changed globally,
     //  this should change to a single tap outside of the bounds.
     internal fun TouchInjectionScope.enterTouchMode() {
         swipe(
-            start = boundsInRoot.center,
-            end = boundsInRoot.bottomCenter + Offset(0f, 10f)
+            start = bounds.center,
+            end = bounds.bottomCenter + Offset(0f, 10f)
         )
     }
 
     // TODO(b/281584353) When touch mode can be changed globally,
     //  this should change to a mouse movement outside of the bounds.
     internal fun enterMouseMode() {
-        mouseDragTo(boundsInRoot.centerLeft, durationMillis = 50)
-        mouseDragTo(boundsInRoot.bottomRight, durationMillis = 50)
-        mouseDragTo(boundsInRoot.center, durationMillis = 50)
+        mouseDragTo(bounds.centerLeft, durationMillis = 50)
+        mouseDragTo(bounds.bottomRight, durationMillis = 50)
+        mouseDragTo(bounds.center, durationMillis = 50)
     }
 
     protected fun performTouchGesture(block: TouchInjectionScope.() -> Unit) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
new file mode 100644
index 0000000..d78a009
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.selection.gestures
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.selection.Selection
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.longPress
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.testutils.TestViewConfiguration
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipe
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.common.truth.Subject
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+class LazyColumnMultiTextRegressionTest {
+    @get:Rule
+    val rule = createComposeRule()
+    private val stateRestorationTester = StateRestorationTester(rule)
+
+    // regression - text going out of composition and then returning
+    // resulted in selection not working
+    @Test
+    fun whenTextScrollsOutOfCompositionAndThenBackIn_creatingSelectionStillPossible() = runTest {
+        scrollDown()
+        scrollUp()
+        createSelection(line = 0)
+        assertSelection().isNotNull()
+    }
+
+    @Test
+    fun whenSelectionScrollsOutOfCompositionAndThenBackIn_selectionRemains() = runTest {
+        createSelection(line = 0)
+        assertSelection().isNotNull()
+        val initialSelection = selection
+        scrollDown()
+        assertSelection().isEqualTo(initialSelection)
+        scrollUp()
+        assertSelection().isEqualTo(initialSelection)
+    }
+
+    // Copy currently doesn't work when the text leaves the view of a lazy layout
+    @Ignore("b/298067102")
+    @Test
+    fun whenTextScrollsOutOfLazyLayoutBounds_copyCorrectlySetsClipboard() = runTest {
+        resetClipboard()
+        createSelection(startLine = 0, endLine = 4)
+        assertSelection().isNotNull()
+        scrollDown()
+        performCopy()
+        assertClipboardTextEquals("01234")
+    }
+
+    // TODO(b/298067619)
+    //  When we support saving selection, this test should instead check that
+    //  the previous and current selection is the same.
+    //  Change test name to reflect this when implemented.
+    @Test
+    fun whenTextIsSavedRestored_clearsSelection() = runTest {
+        createSelection(line = 0)
+        assertSelection().isNotNull()
+        stateRestorationTester.emulateSavedInstanceStateRestore()
+        assertSelection().isNull()
+    }
+
+    private inner class TestScope(
+        private val pointerAreaTag: String,
+        private val selectionState: MutableState<Selection?>,
+        private val clipboardManager: ClipboardManager,
+    ) {
+        val initialText = "Initial text"
+        val selection get() = Snapshot.withoutReadObservation { selectionState.value }
+
+        fun createSelection(startLine: Int, endLine: Int) {
+            performTouchInput {
+                longPress(positionForLine(startLine))
+                moveTo(positionForLine(endLine))
+                up()
+            }
+        }
+
+        fun createSelection(line: Int) {
+            performTouchInput {
+                longClick(positionForLine(line))
+            }
+        }
+
+        private fun performTouchInput(block: TouchInjectionScope.() -> Unit) {
+            rule.onNodeWithTag(pointerAreaTag).performTouchInput(block)
+            rule.waitForIdle()
+        }
+
+        private fun positionForLine(lineNumber: Int): Offset {
+            val containerPosition = rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode()
+                .positionInRoot
+                .also { println(it) }
+
+            val textTag = lineNumber.toString()
+            val textPosition = rule.onNodeWithTag(textTag).fetchSemanticsNode()
+                .positionInRoot
+                .also { println(it) }
+
+            val textLayoutResult = rule.onNodeWithTag(textTag)
+                .fetchTextLayoutResult()
+
+            return textLayoutResult.getBoundingBox(0)
+                .translate(textPosition - containerPosition)
+                .center
+                .also { println(it) }
+        }
+
+        @OptIn(ExperimentalTestApi::class)
+        fun performCopy() {
+            rule.onNodeWithTag(pointerAreaTag).performKeyInput {
+                keyDown(Key.CtrlLeft)
+                keyDown(Key.C)
+                keyUp(Key.C)
+                keyUp(Key.CtrlLeft)
+            }
+            rule.waitForIdle()
+        }
+
+        fun resetClipboard() {
+            clipboardManager.setText(AnnotatedString(initialText))
+        }
+
+        fun assertClipboardTextEquals(text: String) {
+            val actualClipboardText = clipboardManager.getText()?.text
+            assertWithMessage("Clipboard contents was not changed.")
+                .that(actualClipboardText)
+                .isNotEqualTo(initialText)
+            assertWithMessage("""Clipboard set to incorrect content: "$actualClipboardText".""")
+                .that(actualClipboardText)
+                .isEqualTo(text)
+            resetClipboard()
+        }
+
+        fun assertSelection(): Subject = assertThat(selection)
+
+        fun scrollDown() {
+            performTouchInput {
+                swipe(bottomCenter - Offset(0f, 1f), topCenter + Offset(0f, 1f))
+            }
+        }
+
+        fun scrollUp() {
+            performTouchInput {
+                swipe(topCenter + Offset(0f, 1f), bottomCenter - Offset(0f, 1f))
+            }
+        }
+    }
+
+    private fun runTest(block: TestScope.() -> Unit) {
+        val tag = "tag"
+        val selection = mutableStateOf<Selection?>(null)
+        val testViewConfiguration = TestViewConfiguration(
+            minimumTouchTargetSize = DpSize.Zero
+        )
+        lateinit var clipboardManager: ClipboardManager
+        stateRestorationTester.setContent {
+            clipboardManager = LocalClipboardManager.current
+            CompositionLocalProvider(LocalViewConfiguration provides testViewConfiguration) {
+                SelectionContainer(
+                    selection = selection.value,
+                    onSelectionChange = { selection.value = it },
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .wrapContentHeight()
+                ) {
+                    LazyColumn(
+                        modifier = Modifier
+                            .height(100.dp)
+                            .wrapContentHeight()
+                            .testTag(tag)
+                    ) {
+                        items(count = 20) {
+                            BasicText(
+                                text = it.toString(),
+                                style = TextStyle(fontSize = 15.sp, textAlign = TextAlign.Center),
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .testTag(it.toString())
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        val scope = TestScope(tag, selection, clipboardManager)
+        scope.resetClipboard()
+        scope.block()
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesBidiTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesBidiTest.kt
new file mode 100644
index 0000000..5b9e6a7
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesBidiTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.selection.gestures
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.MultiSelectionSubject
+import androidx.compose.foundation.text.selection.gestures.util.TextSelectionAsserter
+import androidx.compose.foundation.text.selection.gestures.util.offsetToLocalOffset
+import androidx.compose.foundation.text.selection.gestures.util.offsetToSelectableId
+import androidx.compose.foundation.text.selection.gestures.util.textContentIndices
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.util.fastForEach
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+internal class MultiTextSelectionGesturesBidiTest : TextSelectionGesturesBidiTest() {
+
+    override val pointerAreaTag = "selectionContainer"
+    private val ltrWord = "hello"
+    private val rtlWord = "בבבבב"
+    override val textContent = mutableStateOf("""
+        $ltrWord $rtlWord $ltrWord
+        $rtlWord $ltrWord $rtlWord
+        $ltrWord $rtlWord $ltrWord
+    """.trimIndent().trim())
+
+    override lateinit var asserter: TextSelectionAsserter
+
+    private lateinit var texts: State<List<Pair<String, String>>>
+    private lateinit var textContentIndices: State<List<IntRange>>
+
+    @Before
+    fun setupAsserter() {
+        asserter = object : TextSelectionAsserter(
+            textContent = textContent.value,
+            rule = rule,
+            textToolbar = textToolbar,
+            hapticFeedback = hapticFeedback,
+            getActual = { selection.value }
+        ) {
+            override fun subAssert() {
+                Truth.assertAbout(MultiSelectionSubject.withContent(texts.value))
+                    .that(getActual())
+                    .hasSelection(
+                        expected = selection,
+                        startTextDirection = startLayoutDirection,
+                        endTextDirection = endLayoutDirection,
+                    )
+            }
+        }
+    }
+
+    @Composable
+    override fun TextContent() {
+        texts = derivedStateOf {
+            textContent.value
+                .split("\n")
+                .withIndex()
+                .map { (index, str) -> str to "testTag$index" }
+        }
+
+        textContentIndices = derivedStateOf { texts.value.textContentIndices() }
+
+        Column {
+            texts.value.fastForEach { (str, tag) ->
+                BasicText(
+                    text = str,
+                    style = TextStyle(
+                        fontFamily = fontFamily,
+                        fontSize = fontSize,
+                    ),
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .testTag(tag),
+                )
+            }
+        }
+    }
+
+    override fun characterPosition(offset: Int, isRtl: Boolean): Offset {
+        val selectableIndex = textContentIndices.value.offsetToSelectableId(offset)
+        val localOffset = textContentIndices.value.offsetToLocalOffset(offset)
+        val (_, tag) = texts.value[selectableIndex]
+        val pointerAreaPosition =
+            rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().positionInRoot
+        val nodePosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
+        val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
+        val boundingBox = textLayoutResult.getBoundingBox(localOffset)
+            .translate(nodePosition - pointerAreaPosition)
+        return if (isRtl) boundingBox.centerRight - Offset(2f, 0f)
+        else boundingBox.centerLeft + Offset(2f, 0f)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesRtlTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesRtlTest.kt
new file mode 100644
index 0000000..8f1013f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesRtlTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.selection.gestures
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.MultiSelectionSubject
+import androidx.compose.foundation.text.selection.gestures.util.TextSelectionAsserter
+import androidx.compose.foundation.text.selection.gestures.util.applyAndAssert
+import androidx.compose.foundation.text.selection.gestures.util.collapsed
+import androidx.compose.foundation.text.selection.gestures.util.offsetToLocalOffset
+import androidx.compose.foundation.text.selection.gestures.util.offsetToSelectableId
+import androidx.compose.foundation.text.selection.gestures.util.textContentIndices
+import androidx.compose.foundation.text.selection.gestures.util.to
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTestApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+internal class MultiTextSelectionGesturesRtlTest : TextSelectionGesturesTest() {
+
+    override val pointerAreaTag = "selectionContainer"
+    override val word = "בבבבב"
+    override val textContent = mutableStateOf("בבבבב\nבבבבב בבבבב בבבבב\nבבבבב")
+    override var textDirection: ResolvedTextDirection = ResolvedTextDirection.Rtl
+
+    override lateinit var asserter: TextSelectionAsserter
+
+    private lateinit var texts: State<List<Pair<String, String>>>
+    private lateinit var textContentIndices: State<List<IntRange>>
+
+    @Before
+    fun setupAsserter() {
+        asserter = object : TextSelectionAsserter(
+            textContent = textContent.value,
+            rule = rule,
+            textToolbar = textToolbar,
+            hapticFeedback = hapticFeedback,
+            getActual = { selection.value }
+        ) {
+            override fun subAssert() {
+                Truth.assertAbout(MultiSelectionSubject.withContent(texts.value))
+                    .that(getActual())
+                    .hasSelection(
+                        expected = selection,
+                        startTextDirection = startLayoutDirection,
+                        endTextDirection = endLayoutDirection,
+                    )
+            }
+        }.apply {
+            startLayoutDirection = ResolvedTextDirection.Rtl
+            endLayoutDirection = ResolvedTextDirection.Rtl
+        }
+    }
+
+    @Composable
+    override fun TextContent() {
+        texts = derivedStateOf {
+            textContent.value
+                .split("\n")
+                .withIndex()
+                .map { (index, str) -> str to "testTag$index" }
+        }
+
+        textContentIndices = derivedStateOf { texts.value.textContentIndices() }
+        CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+            Column {
+                texts.value.fastForEach { (str, tag) ->
+                    BasicText(
+                        text = str,
+                        style = TextStyle(
+                            fontFamily = fontFamily,
+                            fontSize = fontSize,
+                        ),
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .testTag(tag),
+                    )
+                }
+            }
+        }
+    }
+
+    override fun characterPosition(offset: Int): Offset {
+        val selectableIndex = textContentIndices.value.offsetToSelectableId(offset)
+        val localOffset = textContentIndices.value.offsetToLocalOffset(offset)
+        val (_, tag) = texts.value[selectableIndex]
+        val pointerAreaPosition =
+            rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().positionInRoot
+        val nodePosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
+        val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
+        return textLayoutResult.getBoundingBox(localOffset)
+            .translate(nodePosition - pointerAreaPosition)
+            .centerRight
+            .nudge(HorizontalDirection.END)
+    }
+
+    @Test
+    override fun whenMouseCollapsedSelectionAcrossLines_thenTouch_showUi() {
+        performMouseGesture {
+            moveTo(centerEnd)
+            press()
+        }
+
+        asserter.applyAndAssert {
+            selection = 23.collapsed
+        }
+
+        mouseDragTo(characterPosition(offset = 24))
+
+        asserter.applyAndAssert {
+            selection = 23 to 24
+        }
+
+        performTouchGesture {
+            enterTouchMode()
+        }
+
+        asserter.applyAndAssert {
+            selectionHandlesShown = true
+
+            // only difference from the parent function, the selection is empty,
+            // so the toolbar for copying won't appear.
+            textToolbarShown = false
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesTest.kt
index 28bd729..8bc7c6e5 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextSelectionGesturesTest.kt
@@ -31,6 +31,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.testTag
@@ -50,9 +51,11 @@
 @RunWith(AndroidJUnit4::class)
 internal class MultiTextSelectionGesturesTest : TextSelectionGesturesTest() {
 
-    override lateinit var asserter: TextSelectionAsserter
-
     override val pointerAreaTag = "selectionContainer"
+    override val word = "hello"
+    override val textContent = mutableStateOf("line1\nline2 text1 text2\nline3")
+
+    override lateinit var asserter: TextSelectionAsserter
 
     private lateinit var texts: State<List<Pair<String, String>>>
     private lateinit var textContentIndices: State<List<IntRange>>
@@ -69,7 +72,11 @@
             override fun subAssert() {
                 Truth.assertAbout(MultiSelectionSubject.withContent(texts.value))
                     .that(getActual())
-                    .hasSelection(selection)
+                    .hasSelection(
+                        expected = selection,
+                        startTextDirection = startLayoutDirection,
+                        endTextDirection = endLayoutDirection,
+                    )
             }
         }
     }
@@ -105,15 +112,20 @@
         val selectableIndex = textContentIndices.value.offsetToSelectableId(offset)
         val localOffset = textContentIndices.value.offsetToLocalOffset(offset)
         val (_, tag) = texts.value[selectableIndex]
+        val pointerAreaPosition =
+            rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().positionInRoot
         val nodePosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
         val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
-        return textLayoutResult.getBoundingBox(localOffset).translate(nodePosition).center
+        return textLayoutResult.getBoundingBox(localOffset)
+            .translate(nodePosition - pointerAreaPosition)
+            .centerLeft
+            .nudge(HorizontalDirection.END)
     }
 
     @Test
-    fun whenMouseCollapsedSelectionAcrossLines_thenTouch_noUiElements() {
+    override fun whenMouseCollapsedSelectionAcrossLines_thenTouch_showUi() {
         performMouseGesture {
-            moveTo(boundsInRoot.centerRight - Offset(1f, 0f))
+            moveTo(centerEnd)
             press()
         }
 
@@ -121,7 +133,7 @@
             selection = 23.collapsed
         }
 
-        mouseDragTo(characterPosition(24))
+        mouseDragTo(characterPosition(offset = 24))
 
         asserter.applyAndAssert {
             selection = 23 to 24
@@ -131,6 +143,12 @@
             enterTouchMode()
         }
 
-        asserter.assert()
+        asserter.applyAndAssert {
+            selectionHandlesShown = true
+
+            // only difference from the parent function, the selection is empty,
+            // so the toolbar for copying won't appear.
+            textToolbarShown = false
+        }
     }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextWithSpaceSelectionGesturesRegressionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextWithSpaceSelectionGesturesRegressionTest.kt
index b06598b2..dd36af1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextWithSpaceSelectionGesturesRegressionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/MultiTextWithSpaceSelectionGesturesRegressionTest.kt
@@ -97,7 +97,11 @@
             override fun subAssert() {
                 Truth.assertAbout(MultiSelectionSubject.withContent(texts))
                     .that(getActual())
-                    .hasSelection(selection)
+                    .hasSelection(
+                        expected = selection,
+                        startTextDirection = startLayoutDirection,
+                        endTextDirection = endLayoutDirection,
+                    )
             }
         }
     }
@@ -107,9 +111,13 @@
         val selectableIndex = textContentIndices.offsetToSelectableId(offset)
         val localOffset = textContentIndices.offsetToLocalOffset(offset)
         val (_, tag) = texts[selectableIndex]
+        val pointerAreaPosition =
+            rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().positionInRoot
         val nodePosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
         val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
-        return textLayoutResult.getBoundingBox(localOffset).translate(nodePosition).center
+        return textLayoutResult.getBoundingBox(localOffset)
+            .translate(nodePosition - pointerAreaPosition)
+            .centerLeft
     }
 
     // There were cases where moving the cursor outside the bounds of any text would
@@ -131,13 +139,13 @@
             selection = 18 to 23
         }
 
-        mouseDragTo(position = boundsInRoot.topRight + Offset(-1f, 1f))
+        mouseDragTo(topEnd)
 
         asserter.applyAndAssert {
             selection = 24 to 6
         }
 
-        mouseDragTo(position = boundsInRoot.bottomRight + Offset(-1f, -1f))
+        mouseDragTo(bottomEnd)
 
         asserter.applyAndAssert {
             selection = 18 to 30
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesBidiTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesBidiTest.kt
new file mode 100644
index 0000000..866961e
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesBidiTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.selection.gestures
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.SelectionSubject
+import androidx.compose.foundation.text.selection.gestures.util.TextSelectionAsserter
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+internal class SingleTextSelectionGesturesBidiTest : TextSelectionGesturesBidiTest() {
+
+    private val testTag = "testTag"
+    private val ltrWord = "hello"
+    private val rtlWord = "בבבבב"
+    override val textContent = mutableStateOf("""
+        $ltrWord $rtlWord $ltrWord
+        $rtlWord $ltrWord $rtlWord
+        $ltrWord $rtlWord $ltrWord
+    """.trimIndent().trim())
+
+    override lateinit var asserter: TextSelectionAsserter
+
+    @Before
+    fun setupAsserter() {
+        asserter = object : TextSelectionAsserter(
+            textContent = textContent.value,
+            rule = rule,
+            textToolbar = textToolbar,
+            hapticFeedback = hapticFeedback,
+            getActual = { selection.value },
+        ) {
+            override fun subAssert() {
+                Truth.assertAbout(SelectionSubject.withContent(textContent))
+                    .that(getActual())
+                    .hasSelection(
+                        expected = selection,
+                        startTextDirection = startLayoutDirection,
+                        endTextDirection = endLayoutDirection,
+                    )
+            }
+        }
+    }
+
+    @Composable
+    override fun TextContent() {
+        BasicText(
+            text = textContent.value,
+            style = TextStyle(
+                fontFamily = fontFamily,
+                fontSize = fontSize,
+            ),
+            modifier = Modifier
+                .fillMaxWidth()
+                .testTag(testTag),
+        )
+    }
+
+    override fun characterPosition(offset: Int, isRtl: Boolean): Offset {
+        val textLayoutResult = rule.onNodeWithTag(testTag).fetchTextLayoutResult()
+        val boundingBox = textLayoutResult.getBoundingBox(offset)
+        return if (isRtl) boundingBox.centerRight - Offset(2f, 0f)
+        else boundingBox.centerLeft + Offset(2f, 0f)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesRtlTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesRtlTest.kt
new file mode 100644
index 0000000..2948ea5
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesRtlTest.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.selection.gestures
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.SelectionSubject
+import androidx.compose.foundation.text.selection.gestures.util.TextSelectionAsserter
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+internal class SingleTextSelectionGesturesRtlTest : TextSelectionGesturesTest() {
+
+    private val testTag = "testTag"
+
+    override val word = "בבבבב"
+    override val textContent = mutableStateOf("בבבבב\nבבבבב בבבבב בבבבב\nבבבבב")
+    override var textDirection: ResolvedTextDirection = ResolvedTextDirection.Rtl
+
+    override lateinit var asserter: TextSelectionAsserter
+
+    @Before
+    fun setupAsserter() {
+        asserter = object : TextSelectionAsserter(
+            textContent = textContent.value,
+            rule = rule,
+            textToolbar = textToolbar,
+            hapticFeedback = hapticFeedback,
+            getActual = { selection.value },
+        ) {
+            override fun subAssert() {
+                Truth.assertAbout(SelectionSubject.withContent(textContent))
+                    .that(getActual())
+                    .hasSelection(
+                        expected = selection,
+                        startTextDirection = startLayoutDirection,
+                        endTextDirection = endLayoutDirection,
+                    )
+            }
+        }.apply {
+            startLayoutDirection = ResolvedTextDirection.Rtl
+            endLayoutDirection = ResolvedTextDirection.Rtl
+        }
+    }
+
+    @Composable
+    override fun TextContent() {
+        CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+            BasicText(
+                text = textContent.value,
+                style = TextStyle(
+                    fontFamily = fontFamily,
+                    fontSize = fontSize,
+                ),
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .testTag(testTag),
+            )
+        }
+    }
+
+    override fun characterPosition(offset: Int): Offset {
+        val textLayoutResult = rule.onNodeWithTag(testTag).fetchTextLayoutResult()
+        return textLayoutResult.getBoundingBox(offset).centerRight.nudge(HorizontalDirection.END)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesTest.kt
index af1d9f2..ed6e4ec 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/SingleTextSelectionGesturesTest.kt
@@ -21,29 +21,27 @@
 import androidx.compose.foundation.text.selection.fetchTextLayoutResult
 import androidx.compose.foundation.text.selection.gestures.util.SelectionSubject
 import androidx.compose.foundation.text.selection.gestures.util.TextSelectionAsserter
-import androidx.compose.foundation.text.selection.gestures.util.applyAndAssert
-import androidx.compose.foundation.text.selection.gestures.util.collapsed
-import androidx.compose.foundation.text.selection.gestures.util.to
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.text.TextStyle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth
 import org.junit.Before
-import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalTestApi::class)
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 internal class SingleTextSelectionGesturesTest : TextSelectionGesturesTest() {
 
     private val testTag = "testTag"
+    override val word = "hello"
+    override val textContent = mutableStateOf("line1\nline2 text1 text2\nline3")
+
     override lateinit var asserter: TextSelectionAsserter
 
     @Before
@@ -58,7 +56,11 @@
             override fun subAssert() {
                 Truth.assertAbout(SelectionSubject.withContent(textContent))
                     .that(getActual())
-                    .hasSelection(selection)
+                    .hasSelection(
+                        expected = selection,
+                        startTextDirection = startLayoutDirection,
+                        endTextDirection = endLayoutDirection,
+                    )
             }
         }
     }
@@ -78,35 +80,7 @@
     }
 
     override fun characterPosition(offset: Int): Offset {
-        val nodePosition = rule.onNodeWithTag(testTag).fetchSemanticsNode().positionInRoot
         val textLayoutResult = rule.onNodeWithTag(testTag).fetchTextLayoutResult()
-        return textLayoutResult.getBoundingBox(offset).translate(nodePosition).center
-    }
-
-    @Test
-    fun whenMouseCollapsedSelectionAcrossLines_thenTouch_showUi() {
-        performMouseGesture {
-            moveTo(boundsInRoot.centerRight - Offset(1f, 0f))
-            press()
-        }
-
-        asserter.applyAndAssert {
-            selection = 23.collapsed
-        }
-
-        mouseDragTo(characterPosition(offset = 24))
-
-        asserter.applyAndAssert {
-            selection = 23 to 24
-        }
-
-        performTouchGesture {
-            enterTouchMode()
-        }
-
-        asserter.applyAndAssert {
-            selectionHandlesShown = true
-            textToolbarShown = true
-        }
+        return textLayoutResult.getBoundingBox(offset).centerLeft.nudge(HorizontalDirection.END)
     }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesLtrTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesLtrTest.kt
new file mode 100644
index 0000000..52f052a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesLtrTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.selection.gestures
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.TextFieldSelectionAsserter
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+internal class TextFieldSelectionGesturesLtrTest : TextFieldSelectionGesturesTest() {
+
+    override val pointerAreaTag = "testTag"
+
+    private val textContent = "line1\nline2 text1 text2\nline3"
+
+    override val word = "hello"
+    override val textFieldValue = mutableStateOf(TextFieldValue(textContent))
+
+    override lateinit var asserter: TextFieldSelectionAsserter
+
+    @Composable
+    override fun Content() {
+        BasicTextField(
+            value = textFieldValue.value,
+            onValueChange = { textFieldValue.value = it },
+            textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+            modifier = Modifier
+                .fillMaxWidth()
+                .testTag(pointerAreaTag),
+        )
+    }
+
+    override fun characterPosition(offset: Int): Offset {
+        val textLayoutResult = rule.onNodeWithTag(pointerAreaTag).fetchTextLayoutResult()
+        return textLayoutResult.getBoundingBox(offset).centerLeft.nudge(HorizontalDirection.END)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesRtlTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesRtlTest.kt
new file mode 100644
index 0000000..c1bbaef
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesRtlTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.selection.gestures
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.util.TextFieldSelectionAsserter
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+internal class TextFieldSelectionGesturesRtlTest : TextFieldSelectionGesturesTest() {
+
+    override val pointerAreaTag = "testTag"
+
+    private val textContent = "בבבבב\nבבבבב בבבבב בבבבב\nבבבבב"
+
+    override val word = "בבבבב"
+    override val textFieldValue = mutableStateOf(TextFieldValue(textContent))
+    override var textDirection: ResolvedTextDirection = ResolvedTextDirection.Rtl
+
+    override lateinit var asserter: TextFieldSelectionAsserter
+
+    @Composable
+    override fun Content() {
+        CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+            BasicTextField(
+                value = textFieldValue.value,
+                onValueChange = { textFieldValue.value = it },
+                textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .testTag(pointerAreaTag),
+            )
+        }
+    }
+
+    override fun characterPosition(offset: Int): Offset {
+        val textLayoutResult = rule.onNodeWithTag(pointerAreaTag).fetchTextLayoutResult()
+        return textLayoutResult.getBoundingBox(offset).centerRight.nudge(HorizontalDirection.END)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesTest.kt
index 122af22..0263cf2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionGesturesTest.kt
@@ -16,62 +16,44 @@
 
 package androidx.compose.foundation.text.selection.gestures
 
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.foundation.text.selection.gestures.AbstractSelectionGesturesTest.HorizontalDirection.START
+import androidx.compose.foundation.text.selection.gestures.AbstractSelectionGesturesTest.VerticalDirection.DOWN
+import androidx.compose.foundation.text.selection.gestures.AbstractSelectionGesturesTest.VerticalDirection.UP
 import androidx.compose.foundation.text.selection.gestures.util.TextFieldSelectionAsserter
 import androidx.compose.foundation.text.selection.gestures.util.applyAndAssert
 import androidx.compose.foundation.text.selection.gestures.util.collapsed
 import androidx.compose.foundation.text.selection.gestures.util.longPress
 import androidx.compose.foundation.text.selection.gestures.util.to
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.Modifier
+import androidx.compose.runtime.MutableState
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.longClick
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTextInput
 import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.TextFieldValue
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
 import org.junit.Before
 import org.junit.Test
-import org.junit.runner.RunWith
 
 @OptIn(ExperimentalTestApi::class)
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-internal class TextFieldSelectionGesturesTest : AbstractSelectionGesturesTest() {
+internal abstract class TextFieldSelectionGesturesTest : AbstractSelectionGesturesTest() {
 
     override val pointerAreaTag = "testTag"
-    private val textContent = "line1\nline2 text1 text2\nline3"
 
-    private val textFieldValue = mutableStateOf(TextFieldValue(textContent))
+    /**
+     * Word to use in one-off tests. Subclasses may choose a RTL or BiDi 5 letter word for example.
+     */
+    protected abstract val word: String
+    protected abstract val textFieldValue: MutableState<TextFieldValue>
+    protected abstract var asserter: TextFieldSelectionAsserter
 
-    private lateinit var asserter: TextFieldSelectionAsserter
-
-    @Composable
-    override fun Content() {
-        BasicTextField(
-            value = textFieldValue.value,
-            onValueChange = { textFieldValue.value = it },
-            textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
-            modifier = Modifier
-                .fillMaxWidth()
-                .testTag(pointerAreaTag),
-        )
-    }
+    protected abstract fun characterPosition(offset: Int): Offset
 
     @Before
     fun setupAsserter() {
         asserter = TextFieldSelectionAsserter(
-            textContent = textContent,
+            textContent = textFieldValue.value.text,
             rule = rule,
             textToolbar = textToolbar,
             hapticFeedback = hapticFeedback,
@@ -80,12 +62,23 @@
     }
 
     @Test
+    fun whenTouch_withLongPressOutOfBounds_nothingHappens() {
+        performTouchGesture {
+            longPress(topStart.nudge(yDirection = UP))
+        }
+
+        asserter.assert()
+        touchDragTo(topEnd.nudge(yDirection = UP))
+        asserter.assert()
+    }
+
+    @Test
     fun whenTouch_withNoTextThenLongPress_noSelection() {
         textFieldValue.value = TextFieldValue()
         rule.waitForIdle()
 
         performTouchGesture {
-            longClick(boundsInRoot.center)
+            longClick(center)
         }
 
         asserter.applyAndAssert {
@@ -101,7 +94,7 @@
         rule.waitForIdle()
 
         performTouchGesture {
-            longPress(boundsInRoot.center)
+            longPress(center)
         }
 
         asserter.applyAndAssert {
@@ -109,7 +102,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.centerLeft)
+        touchDragTo(centerStart)
 
         asserter.assert()
 
@@ -130,7 +123,7 @@
         rule.waitForIdle()
 
         performTouchGesture {
-            longPress(boundsInRoot.center)
+            longPress(center)
         }
 
         asserter.applyAndAssert {
@@ -138,7 +131,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.centerLeft)
+        touchDragTo(centerStart)
 
         asserter.assert()
 
@@ -210,6 +203,111 @@
     }
 
     @Test
+    fun whenTouch_withLongPressThenDragLeftOutOfBounds_keepsFirstCharSelected() {
+        performTouchGesture {
+            longPress(characterPosition(9))
+        }
+
+        asserter.applyAndAssert {
+            selection = 6 to 11
+            selectionHandlesShown = true
+            magnifierShown = true
+            hapticsCount++
+        }
+
+        touchDragTo(centerStart)
+        // drag to just inside the left bound, one char should remain selected
+        asserter.applyAndAssert {
+            selection = 6 to 7
+            hapticsCount++
+        }
+
+        touchDragTo(centerStart.nudge(START))
+        // drag just outside of the left bound, should be no change.
+        // Regression: we want to ensure the selection doesn't travel to a line above the cursor
+        asserter.assert()
+
+        performTouchGesture {
+            up()
+        }
+
+        asserter.applyAndAssert {
+            textToolbarShown = true
+            magnifierShown = false
+        }
+    }
+
+    @Test
+    fun whenTouch_withLongPressThenDragLeftOutOfBoundsUpAndDown_selectsLines() {
+        performTouchGesture {
+            longPress(characterPosition(9))
+        }
+
+        // anchor starts at beginning of middle line
+        asserter.applyAndAssert {
+            selection = 6 to 11
+            selectionHandlesShown = true
+            magnifierShown = true
+            hapticsCount++
+        }
+
+        // beginning of middle line
+        touchDragTo(characterPosition(6) + Offset(-2f, 0f))
+        asserter.applyAndAssert {
+            selection = 6 to 7
+            hapticsCount++
+        }
+
+        // beginning of top line
+        touchDragTo(characterPosition(0) + Offset(-2f, 0f))
+        asserter.applyAndAssert {
+            selection = 6 to 0
+            hapticsCount++
+        }
+
+        // above top line
+        touchDragTo(topStart.nudge(yDirection = UP))
+        asserter.assert()
+
+        // below bottom line, should be end of text completely
+        touchDragTo(bottomStart.nudge(yDirection = DOWN))
+        asserter.applyAndAssert {
+            selection = 6 to 29
+            hapticsCount++
+        }
+    }
+
+    @Test
+    fun whenTouch_verifyOneCharStaysSelected_withinLine() {
+        performTouchGesture {
+            longPress(characterPosition(14))
+        }
+
+        asserter.applyAndAssert {
+            selection = 12 to 17
+            selectionHandlesShown = true
+            magnifierShown = true
+            hapticsCount++
+        }
+
+        touchDragTo(characterPosition(13))
+        asserter.applyAndAssert {
+            selection = 12 to 13
+            hapticsCount++
+        }
+
+        touchDragTo(characterPosition(12))
+        // shouldn't allow collapsed selection, but keeps previous single char selection
+        asserter.assert()
+
+        touchDragTo(characterPosition(11))
+        asserter.applyAndAssert {
+            selection = 12 to 11
+            hapticsCount++
+        }
+    }
+
+    @Test
     fun whenTouch_withLongPress_selectsSingleWord() {
         performTouchGesture {
             longClick(characterPosition(13))
@@ -333,7 +431,7 @@
     @Test
     fun whenTouch_withLongPressThenDragToUpperEndPaddingAndBack_selectsWordsThenChars() {
         touchLongPressThenDragToEndPaddingTest(
-            endOffset = boundsInRoot.topRight + Offset(-1f, 1f),
+            endOffset = topEnd,
             endSelection = 12 to 0,
         )
     }
@@ -341,7 +439,7 @@
     @Test
     fun whenTouch_withLongPressThenDragToMiddleEndPaddingAndBack_selectsWordsThenChars() {
         touchLongPressThenDragToEndPaddingTest(
-            endOffset = boundsInRoot.centerRight + Offset(-1f, 0f),
+            endOffset = centerEnd,
             endSelection = 12 to 23,
         )
     }
@@ -349,7 +447,7 @@
     @Test
     fun whenTouch_withLongPressThenDragToLowerEndPaddingAndBack_selectsWordsThenChars() {
         touchLongPressThenDragToEndPaddingTest(
-            endOffset = boundsInRoot.bottomRight + Offset(-1f, -1f),
+            endOffset = bottomEnd,
             endSelection = 12 to 29,
         )
     }
@@ -389,7 +487,7 @@
     @Test
     fun whenTouch_withLongPressInEndPadding_entersSelectionMode() {
         performTouchGesture {
-            longPress(boundsInRoot.topRight + Offset(-1f, 1f))
+            longPress(topEnd)
         }
 
         asserter.applyAndAssert {
@@ -414,12 +512,12 @@
 
     @Test
     fun whenTouch_withLongPressInEndPaddingOfEmptyLine_entersSelectionMode() {
-        val content = "Line1\n\nLine3"
+        val content = "$word\n\n$word"
         textFieldValue.value = TextFieldValue(content)
         rule.waitForIdle()
 
         performTouchGesture {
-            longPress(boundsInRoot.centerRight + Offset(-1f, 0f))
+            longPress(centerEnd)
         }
 
         asserter.applyAndAssert {
@@ -446,7 +544,7 @@
     @Test
     fun whenTouch_withLongPressInEndPaddingThenDragToUpperEndPadding_selectsParagraphAndNewLine() {
         performTouchGesture {
-            longPress(boundsInRoot.centerRight + Offset(-1f, 0f))
+            longPress(centerEnd)
         }
 
         asserter.applyAndAssert {
@@ -455,7 +553,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.topRight + Offset(-1f, 1f))
+        touchDragTo(topEnd)
 
         asserter.applyAndAssert {
             selection = 23 to 0
@@ -476,7 +574,7 @@
     @Test
     fun whenTouch_withLongPressInEndPaddingThenDragToLowerEndPadding_selectsNewLineAndParagraph() {
         performTouchGesture {
-            longPress(boundsInRoot.centerRight + Offset(-1f, 0f))
+            longPress(centerEnd)
         }
 
         asserter.applyAndAssert {
@@ -485,7 +583,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.bottomRight + Offset(-1f, -1f))
+        touchDragTo(bottomEnd)
 
         asserter.applyAndAssert {
             selection = 23 to 29
@@ -506,7 +604,7 @@
     @Test
     fun whenTouch_withLongPressInEndPaddingOfFinalLine_entersSelectionMode() {
         performTouchGesture {
-            longPress(boundsInRoot.bottomRight + Offset(-1f, -1f))
+            longPress(bottomEnd)
         }
 
         asserter.applyAndAssert {
@@ -568,12 +666,12 @@
 
     @Test
     fun whenTouch_withLongPressInEndPaddingOfEmptyFinalLine_entersSelectionMode() {
-        val content = "Line1\n\n"
+        val content = "$word\n\n"
         textFieldValue.value = TextFieldValue(content)
         rule.waitForIdle()
 
         performTouchGesture {
-            longPress(boundsInRoot.bottomRight + Offset(-1f, -1f))
+            longPress(bottomEnd)
         }
 
         asserter.applyAndAssert {
@@ -748,7 +846,7 @@
     fun whenMouse_withTripleClickThenDragLeft_selectsParagraphs() {
         mouseTripleClickThenDragTest(
             endOffset = characterPosition(8),
-            endSelection = 23 to 6,
+            endSelection = 6 to 23,
         )
     }
 
@@ -812,7 +910,7 @@
 
     // regression test for when selections would overflow onto previous line
     private fun mouseFirstLetterOfLineClicksTest(numClicks: Int, selection: TextRange) {
-        val initialClickOffset = characterBox(6).centerLeft + Offset(1f, 0f)
+        val initialClickOffset = characterPosition(6)
         mouseClicksThenDragTest(
             numClicks = numClicks,
             startOffset = initialClickOffset,
@@ -848,11 +946,11 @@
 
     // regression test for when selections would overflow onto next line
     private fun mouseEndPaddingClicksTest(numClicks: Int, selection: TextRange) {
-        val initialClickOffset = boundsInRoot.centerRight + Offset(-1f, 0f)
+        val initialClickOffset = centerEnd
         mouseClicksThenDragTest(
             numClicks = numClicks,
             startOffset = initialClickOffset,
-            endOffset = initialClickOffset + Offset(0f, 1f),
+            endOffset = initialClickOffset.nudge(yDirection = DOWN),
             startSelection = selection,
             endSelection = selection,
         )
@@ -978,19 +1076,11 @@
     @Test
     fun whenMouse_thenTripleClickInEndPadding_selectsCurrentParagraph() {
         performMouseGesture {
-            repeat(3) { click(boundsInRoot.centerRight - Offset(1f, 0f)) }
+            repeat(3) { click(centerEnd) }
         }
 
         asserter.applyAndAssert {
             selection = 6 to 23
         }
     }
-
-    private fun characterPosition(offset: Int): Offset = characterBox(offset).center
-
-    private fun characterBox(offset: Int): Rect {
-        val nodePosition = rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().positionInRoot
-        val textLayoutResult = rule.onNodeWithTag(pointerAreaTag).fetchTextLayoutResult()
-        return textLayoutResult.getBoundingBox(offset).translate(nodePosition)
-    }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionHandlesGesturesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionHandlesGesturesTest.kt
index a61a929..a7dcdda 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionHandlesGesturesTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextFieldSelectionHandlesGesturesTest.kt
@@ -28,7 +28,6 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.longClick
@@ -126,11 +125,8 @@
         performTouchInput { up() }
     }
 
-    private fun characterPosition(offset: Int): Offset = characterBox(offset).center
-
-    private fun characterBox(offset: Int): Rect {
-        val nodePosition = rule.onNodeWithTag(pointerAreaTag).fetchSemanticsNode().positionInRoot
+    private fun characterPosition(offset: Int): Offset {
         val textLayoutResult = rule.onNodeWithTag(pointerAreaTag).fetchTextLayoutResult()
-        return textLayoutResult.getBoundingBox(offset).translate(nodePosition)
+        return textLayoutResult.getBoundingBox(offset).centerLeft
     }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesBidiTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesBidiTest.kt
new file mode 100644
index 0000000..e3a0d4d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesBidiTest.kt
@@ -0,0 +1,444 @@
+/*
+ * 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.selection.gestures
+
+import androidx.compose.foundation.text.selection.Selection
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.text.selection.gestures.util.TextSelectionAsserter
+import androidx.compose.foundation.text.selection.gestures.util.applyAndAssert
+import androidx.compose.foundation.text.selection.gestures.util.collapsed
+import androidx.compose.foundation.text.selection.gestures.util.longPress
+import androidx.compose.foundation.text.selection.gestures.util.to
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import org.junit.Test
+
+@OptIn(ExperimentalTestApi::class)
+internal abstract class TextSelectionGesturesBidiTest : AbstractSelectionGesturesTest() {
+
+    override val pointerAreaTag = "selectionContainer"
+
+    protected val selection = mutableStateOf<Selection?>(null)
+
+    protected abstract val textContent: MutableState<String>
+    protected abstract var asserter: TextSelectionAsserter
+
+    @Composable
+    abstract fun TextContent()
+
+    @Composable
+    override fun Content() {
+        SelectionContainer(
+            selection = selection.value,
+            onSelectionChange = { selection.value = it },
+            modifier = Modifier.testTag(pointerAreaTag)
+        ) {
+            TextContent()
+        }
+    }
+
+    protected abstract fun characterPosition(offset: Int, isRtl: Boolean): Offset
+
+    @Test
+    fun whenTouch_withLongPress_selectsSingleWord() {
+        performTouchGesture {
+            longClick(characterPosition(26, isRtl = false))
+        }
+
+        asserter.applyAndAssert {
+            selection = 24 to 29
+            selectionHandlesShown = true
+            textToolbarShown = true
+            hapticsCount++
+        }
+    }
+
+    @Test
+    fun whenTouch_withLongPressThenDragLeftAndBack_selectsWordsThenChars() {
+        touchLongPressThenDragForwardsAndBackTest(
+            forwardOffset = characterPosition(22, isRtl = true),
+            forwardSelection = 24 to 22,
+            forwardEndDirection = ResolvedTextDirection.Rtl,
+            backwardOffset = characterPosition(19, isRtl = true),
+            backwardSelection = 24 to 19,
+            backwardEndDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenTouch_withLongPressThenDragUpAndBack_ltrToRtl_selectsWordsThenChars() {
+        touchLongPressThenDragForwardsAndBackTest(
+            forwardOffset = characterPosition(3, isRtl = false),
+            forwardSelection = 24 to 0,
+            forwardEndDirection = ResolvedTextDirection.Ltr,
+            backwardOffset = characterPosition(8, isRtl = true),
+            backwardSelection = 24 to 8,
+            backwardEndDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenTouch_withLongPressThenDragUpAndBack_rtlToLtr_selectsWordsThenChars() {
+        touchLongPressThenDragForwardsAndBackTest(
+            forwardOffset = characterPosition(8, isRtl = true),
+            forwardSelection = 24 to 6,
+            forwardEndDirection = ResolvedTextDirection.Rtl,
+            backwardOffset = characterPosition(13, isRtl = false),
+            backwardSelection = 24 to 13,
+            backwardEndDirection = ResolvedTextDirection.Ltr,
+        )
+    }
+
+    @Test
+    fun whenTouch_withLongPressThenDragRightAndBack_selectsWordsThenChars() {
+        touchLongPressThenDragForwardsAndBackTest(
+            forwardOffset = characterPosition(31, isRtl = true),
+            forwardSelection = 24 to 31,
+            forwardEndDirection = ResolvedTextDirection.Rtl,
+            backwardOffset = characterPosition(34, isRtl = true),
+            backwardSelection = 24 to 34,
+            backwardEndDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenTouch_withLongPressThenDragDownAndBack_ltrToRtl_selectsWordsThenChars() {
+        touchLongPressThenDragForwardsAndBackTest(
+            forwardOffset = characterPosition(51, isRtl = false),
+            forwardSelection = 24 to 53,
+            forwardEndDirection = ResolvedTextDirection.Ltr,
+            backwardOffset = characterPosition(44, isRtl = true),
+            backwardSelection = 24 to 44,
+            backwardEndDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenTouch_withLongPressThenDragDownAndBack_rtlToLtr_selectsWordsThenChars() {
+        touchLongPressThenDragForwardsAndBackTest(
+            forwardOffset = characterPosition(44, isRtl = true),
+            forwardSelection = 24 to 47,
+            forwardEndDirection = ResolvedTextDirection.Ltr,
+            backwardOffset = characterPosition(38, isRtl = false),
+            backwardSelection = 24 to 38,
+            backwardEndDirection = ResolvedTextDirection.Ltr,
+        )
+    }
+
+    private fun touchLongPressThenDragForwardsAndBackTest(
+        forwardOffset: Offset,
+        forwardSelection: TextRange?,
+        forwardEndDirection: ResolvedTextDirection,
+        backwardOffset: Offset,
+        backwardSelection: TextRange?,
+        backwardEndDirection: ResolvedTextDirection,
+    ) {
+        performTouchGesture {
+            longPress(characterPosition(26, isRtl = false))
+        }
+
+        asserter.applyAndAssert {
+            selection = 24 to 29
+            selectionHandlesShown = true
+            magnifierShown = true
+            hapticsCount++
+        }
+
+        touchDragTo(forwardOffset)
+
+        asserter.applyAndAssert {
+            selection = forwardSelection
+            endLayoutDirection = forwardEndDirection
+            hapticsCount++
+        }
+
+        touchDragTo(backwardOffset)
+
+        asserter.applyAndAssert {
+            selection = backwardSelection
+            endLayoutDirection = backwardEndDirection
+            hapticsCount++
+        }
+
+        performTouchGesture {
+            up()
+        }
+
+        asserter.applyAndAssert {
+            textToolbarShown = true
+            magnifierShown = false
+        }
+    }
+
+    @Test
+    fun whenMouse_withSingleClick_collapsedSelectionAtClick() {
+        performMouseGesture {
+            click(characterPosition(26, isRtl = false))
+        }
+
+        asserter.applyAndAssert {
+            selection = 26.collapsed
+        }
+    }
+
+    @Test
+    fun whenMouse_withSingleClickThenRelease_collapsedSelection() {
+        performMouseGesture {
+            moveTo(position = characterPosition(26, isRtl = false))
+            press()
+        }
+
+        asserter.applyAndAssert {
+            selection = 26.collapsed
+        }
+
+        performMouseGesture {
+            release()
+        }
+
+        asserter.assert()
+    }
+
+    @Test
+    fun whenMouse_withSingleClickThenDragLeft_selectsCharacters() {
+        mouseSingleClickThenDragTest(
+            endOffset = characterPosition(21, isRtl = true),
+            endSelection = 26 to 21,
+        )
+    }
+
+    @Test
+    fun whenMouse_withSingleClickThenDragUp_selectsCharacters() {
+        mouseSingleClickThenDragTest(
+            endOffset = characterPosition(8, isRtl = true),
+            endSelection = 26 to 8,
+        )
+    }
+
+    @Test
+    fun whenMouse_withSingleClickThenDragRight_selectsCharacters() {
+        mouseSingleClickThenDragTest(
+            endOffset = characterPosition(32, isRtl = true),
+            endSelection = 26 to 32,
+        )
+    }
+
+    @Test
+    fun whenMouse_withSingleClickThenDragDown_selectsCharacters() {
+        mouseSingleClickThenDragTest(
+            endOffset = characterPosition(44, isRtl = true),
+            endSelection = 26 to 44,
+        )
+    }
+
+    private fun mouseSingleClickThenDragTest(
+        endOffset: Offset,
+        endSelection: TextRange?
+    ) {
+        mouseClicksThenDragTest(
+            numClicks = 1,
+            firstOffset = characterPosition(26, isRtl = false),
+            firstSelection = 26.collapsed,
+            firstStartDirection = ResolvedTextDirection.Ltr,
+            secondOffset = endOffset,
+            secondSelection = endSelection,
+            secondStartDirection = ResolvedTextDirection.Ltr,
+            secondEndDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenMouse_withDoubleClick_selectsWord() {
+        performMouseGesture {
+            repeat(2) { click(characterPosition(26, isRtl = false)) }
+        }
+
+        asserter.applyAndAssert {
+            selection = 24 to 29
+        }
+    }
+
+    @Test
+    fun whenMouse_withDoubleClickThenDragLeft_selectsWords() {
+        mouseDoubleClickThenDragTest(
+            endOffset = characterPosition(21, isRtl = true),
+            endSelection = 29 to 18,
+            endDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenMouse_withDoubleClickThenDragUp_selectsWords() {
+        mouseDoubleClickThenDragTest(
+            endOffset = characterPosition(8, isRtl = true),
+            endSelection = 29 to 6,
+            endDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenMouse_withDoubleClickThenDragRight_selectsWords() {
+        mouseDoubleClickThenDragTest(
+            endOffset = characterPosition(32, isRtl = true),
+            endSelection = 24 to 35,
+            endDirection = ResolvedTextDirection.Ltr,
+        )
+    }
+
+    @Test
+    fun whenMouse_withDoubleClickThenDragDown_selectsWords() {
+        mouseDoubleClickThenDragTest(
+            endOffset = characterPosition(44, isRtl = true),
+            endSelection = 24 to 47,
+            endDirection = ResolvedTextDirection.Ltr,
+        )
+    }
+
+    private fun mouseDoubleClickThenDragTest(
+        endOffset: Offset,
+        endSelection: TextRange?,
+        endDirection: ResolvedTextDirection,
+    ) {
+        mouseClicksThenDragTest(
+            numClicks = 2,
+            firstOffset = characterPosition(26, isRtl = false),
+            firstSelection = 24 to 29,
+            firstStartDirection = ResolvedTextDirection.Ltr,
+            secondOffset = endOffset,
+            secondSelection = endSelection,
+            secondStartDirection = ResolvedTextDirection.Ltr,
+            secondEndDirection = endDirection,
+        )
+    }
+
+    @Test
+    fun whenMouse_withTripleClick_selectsParagraph() {
+        performMouseGesture {
+            repeat(3) { click(characterPosition(26, isRtl = false)) }
+        }
+
+        asserter.applyAndAssert {
+            selection = 18 to 35
+            startLayoutDirection = ResolvedTextDirection.Rtl
+        }
+    }
+
+    @Test
+    fun whenMouse_withTripleClickThenDragLeft_selectsParagraphs() {
+        mouseTripleClickThenDragTest(
+            endOffset = characterPosition(21, isRtl = true),
+            endSelection = 18 to 35,
+            startDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenMouse_withTripleClickThenDragUp_selectsParagraphs() {
+        mouseTripleClickThenDragTest(
+            endOffset = characterPosition(8, isRtl = true),
+            endSelection = 35 to 0,
+            startDirection = ResolvedTextDirection.Ltr,
+        )
+    }
+
+    @Test
+    fun whenMouse_withTripleClickThenDragRight_selectsParagraphs() {
+        mouseTripleClickThenDragTest(
+            endOffset = characterPosition(32, isRtl = true),
+            endSelection = 18 to 35,
+            startDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    @Test
+    fun whenMouse_withTripleClickThenDragDown_selectsParagraphs() {
+        mouseTripleClickThenDragTest(
+            endOffset = characterPosition(44, isRtl = true),
+            endSelection = 18 to 53,
+            startDirection = ResolvedTextDirection.Rtl,
+        )
+    }
+
+    private fun mouseTripleClickThenDragTest(
+        endOffset: Offset,
+        endSelection: TextRange?,
+        startDirection: ResolvedTextDirection,
+    ) {
+        mouseClicksThenDragTest(
+            numClicks = 3,
+            firstOffset = characterPosition(26, isRtl = false),
+            firstSelection = 18 to 35,
+            firstStartDirection = ResolvedTextDirection.Rtl,
+            secondOffset = endOffset,
+            secondSelection = endSelection,
+            secondStartDirection = startDirection,
+            secondEndDirection = ResolvedTextDirection.Ltr,
+        )
+    }
+
+    private fun mouseClicksThenDragTest(
+        numClicks: Int,
+        firstOffset: Offset,
+        firstSelection: TextRange?,
+        firstStartDirection: ResolvedTextDirection,
+        secondOffset: Offset,
+        secondSelection: TextRange?,
+        secondStartDirection: ResolvedTextDirection,
+        secondEndDirection: ResolvedTextDirection,
+    ) {
+        check(numClicks > 0) { "Must be at least one click" }
+        performMouseGesture {
+            moveTo(firstOffset)
+            press()
+            repeat(numClicks - 1) {
+                advanceEventTime()
+                release()
+                advanceEventTime()
+                press()
+            }
+        }
+
+        asserter.applyAndAssert {
+            selection = firstSelection
+            startLayoutDirection = firstStartDirection
+            endLayoutDirection = ResolvedTextDirection.Ltr
+        }
+
+        mouseDragTo(secondOffset)
+
+        asserter.applyAndAssert {
+            selection = secondSelection
+            startLayoutDirection = secondStartDirection
+            endLayoutDirection = secondEndDirection
+        }
+
+        performMouseGesture {
+            release()
+        }
+
+        asserter.assert()
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesTest.kt
index fec14b8..5206293 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionGesturesTest.kt
@@ -18,12 +18,16 @@
 
 import androidx.compose.foundation.text.selection.Selection
 import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.text.selection.gestures.AbstractSelectionGesturesTest.HorizontalDirection.START
+import androidx.compose.foundation.text.selection.gestures.AbstractSelectionGesturesTest.VerticalDirection.DOWN
+import androidx.compose.foundation.text.selection.gestures.AbstractSelectionGesturesTest.VerticalDirection.UP
 import androidx.compose.foundation.text.selection.gestures.util.TextSelectionAsserter
 import androidx.compose.foundation.text.selection.gestures.util.applyAndAssert
 import androidx.compose.foundation.text.selection.gestures.util.collapsed
 import androidx.compose.foundation.text.selection.gestures.util.longPress
 import androidx.compose.foundation.text.selection.gestures.util.to
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
@@ -39,9 +43,13 @@
 
     override val pointerAreaTag = "selectionContainer"
 
-    protected val textContent = mutableStateOf("line1\nline2 text1 text2\nline3")
     protected val selection = mutableStateOf<Selection?>(null)
 
+    /**
+     * Word to use in one-off tests. Subclasses may choose a RTL or BiDi 5 letter word for example.
+     */
+    protected abstract val word: String
+    protected abstract val textContent: MutableState<String>
     protected abstract var asserter: TextSelectionAsserter
 
     @Composable
@@ -61,6 +69,17 @@
     protected abstract fun characterPosition(offset: Int): Offset
 
     @Test
+    fun whenTouch_withLongPressOutOfBounds_nothingHappens() {
+        performTouchGesture {
+            longPress(topStart.nudge(yDirection = UP))
+        }
+
+        asserter.assert()
+        touchDragTo(topEnd.nudge(yDirection = UP))
+        asserter.assert()
+    }
+
+    @Test
     fun whenTouch_withNoTextThenLongPress_noSelection() {
         val content = ""
         textContent.value = content
@@ -68,12 +87,11 @@
         rule.waitForIdle()
 
         performTouchGesture {
-            longClick(boundsInRoot.center)
+            longClick(center)
         }
 
         asserter.applyAndAssert {
             selection = 0.collapsed
-            hapticsCount++
         }
     }
 
@@ -85,15 +103,14 @@
         rule.waitForIdle()
 
         performTouchGesture {
-            longPress(boundsInRoot.center)
+            longPress(center)
         }
 
         asserter.applyAndAssert {
             selection = 0.collapsed
-            hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.centerLeft)
+        touchDragTo(centerStart)
 
         asserter.assert()
 
@@ -106,13 +123,13 @@
 
     @Test
     fun whenTouch_withLongPressInEndPaddingOfEmptyLine_entersSelectionMode() {
-        val content = "Line1\n\nLine3"
+        val content = "$word\n\n$word"
         textContent.value = content
         asserter.textContent = content
         rule.waitForIdle()
 
         performTouchGesture {
-            longPress(boundsInRoot.centerRight + Offset(-1f, 0f))
+            longPress(centerEnd)
         }
 
         asserter.applyAndAssert {
@@ -134,30 +151,70 @@
 
     @Test
     fun whenTouch_withLongPressInEndPaddingOfEmptyFinalLine_entersSelectionMode() {
-        val content = "Line1\n\n"
+        val content = "$word\n\n"
         textContent.value = content
         asserter.textContent = content
         rule.waitForIdle()
 
         performTouchGesture {
-            longPress(boundsInRoot.bottomRight + Offset(-1f, -1f))
+            longPress(bottomEnd)
         }
 
         asserter.applyAndAssert {
             selection = 7.collapsed
+            selectionHandlesShown = false
             hapticsCount++
         }
 
         // we want to test at least one drag that shouldn't affect selection as well
-        touchDragBy(Offset(-1f, 0f))
+        touchDragBy(Offset.Zero.nudge(START))
 
         asserter.assert()
 
-        performTouchGesture {
-            up()
+        touchDragTo(centerEnd)
+
+        asserter.applyAndAssert {
+            selection = 7 to 6
+            selectionHandlesShown = true
+            hapticsCount++
         }
 
-        asserter.assert()
+        touchDragTo(topEnd)
+
+        asserter.applyAndAssert {
+            selection = 7 to 0
+            hapticsCount++
+        }
+
+        touchDragTo(topStart)
+
+        asserter.applyAndAssert {
+            magnifierShown = true
+        }
+
+        // take a stop in the middle of the word, otherwise we may not shrink selection at all,
+        // and then word adjustment will be used once we move pointer to the end of the line.
+        touchDragTo(characterPosition(4))
+
+        asserter.applyAndAssert {
+            selection = 7 to 4
+            hapticsCount++
+        }
+
+        touchDragTo(topEnd)
+
+        asserter.applyAndAssert {
+            selection = 7 to 5
+            magnifierShown = false // pointer is too far from text to show magnifier
+            hapticsCount++
+        }
+
+        touchDragTo(centerEnd)
+
+        asserter.applyAndAssert {
+            selection = 7 to 6
+            hapticsCount++
+        }
     }
 
     @Test
@@ -186,6 +243,111 @@
     }
 
     @Test
+    fun whenTouch_withLongPressThenDragLeftOutOfBounds_keepsFirstCharSelected() {
+        performTouchGesture {
+            longPress(characterPosition(9))
+        }
+
+        asserter.applyAndAssert {
+            selection = 6 to 11
+            selectionHandlesShown = true
+            magnifierShown = true
+            hapticsCount++
+        }
+
+        touchDragTo(centerStart)
+        // drag to just inside the left bound, one char should remain selected
+        asserter.applyAndAssert {
+            selection = 6 to 7
+            hapticsCount++
+        }
+
+        touchDragTo(centerStart.nudge(START))
+        // drag just outside of the left bound, should be no change.
+        // Regression: we want to ensure the selection doesn't travel to a line above the cursor
+        asserter.assert()
+
+        performTouchGesture {
+            up()
+        }
+
+        asserter.applyAndAssert {
+            textToolbarShown = true
+            magnifierShown = false
+        }
+    }
+
+    @Test
+    fun whenTouch_withLongPressThenDragLeftOutOfBoundsUpAndDown_selectsLines() {
+        performTouchGesture {
+            longPress(characterPosition(9))
+        }
+
+        // anchor starts at beginning of middle line
+        asserter.applyAndAssert {
+            selection = 6 to 11
+            selectionHandlesShown = true
+            magnifierShown = true
+            hapticsCount++
+        }
+
+        // beginning of middle line
+        touchDragTo(characterPosition(6) + Offset(-2f, 0f))
+        asserter.applyAndAssert {
+            selection = 6 to 7
+            hapticsCount++
+        }
+
+        // beginning of top line
+        touchDragTo(characterPosition(0) + Offset(-2f, 0f))
+        asserter.applyAndAssert {
+            selection = 6 to 0
+            hapticsCount++
+        }
+
+        // above top line
+        touchDragTo(topStart.nudge(START, UP))
+        asserter.assert()
+
+        // below bottom line, should be end of text completely
+        touchDragTo(bottomStart.nudge(START, DOWN))
+        asserter.applyAndAssert {
+            selection = 6 to 29
+            hapticsCount++
+        }
+    }
+
+    @Test
+    fun whenTouch_verifyOneCharStaysSelected_withinLine() {
+        performTouchGesture {
+            longPress(characterPosition(14))
+        }
+
+        asserter.applyAndAssert {
+            selection = 12 to 17
+            selectionHandlesShown = true
+            magnifierShown = true
+            hapticsCount++
+        }
+
+        touchDragTo(characterPosition(13))
+        asserter.applyAndAssert {
+            selection = 12 to 13
+            hapticsCount++
+        }
+
+        touchDragTo(characterPosition(12))
+        // shouldn't allow collapsed selection, but keeps previous single char selection
+        asserter.assert()
+
+        touchDragTo(characterPosition(11))
+        asserter.applyAndAssert {
+            selection = 12 to 11
+            hapticsCount++
+        }
+    }
+
+    @Test
     fun whenTouch_withLongPress_selectsSingleWord() {
         performTouchGesture {
             longClick(characterPosition(13))
@@ -235,7 +397,7 @@
     @Test
     fun whenTouch_withLongPressThenDragUpAndBack_selectsWordsThenChars() {
         touchLongPressThenDragForwardsAndBackTest(
-            forwardOffset = characterPosition(1),
+            forwardOffset = characterPosition(0),
             forwardSelection = 12 to 0,
             backwardOffset = characterPosition(3),
             backwardSelection = 12 to 3,
@@ -309,7 +471,7 @@
     @Test
     fun whenTouch_withLongPressThenDragToUpperEndPaddingAndBack_selectsWordsThenChars() {
         touchLongPressThenDragToEndPaddingTest(
-            endOffset = boundsInRoot.topRight + Offset(-1f, 1f),
+            endOffset = topEnd,
             endSelection = 12 to 0,
         )
     }
@@ -317,7 +479,7 @@
     @Test
     fun whenTouch_withLongPressThenDragToMiddleEndPaddingAndBack_selectsWordsThenChars() {
         touchLongPressThenDragToEndPaddingTest(
-            endOffset = boundsInRoot.centerRight + Offset(-1f, 0f),
+            endOffset = centerEnd,
             endSelection = 12 to 23,
         )
     }
@@ -325,7 +487,7 @@
     @Test
     fun whenTouch_withLongPressThenDragToLowerEndPaddingAndBack_selectsWordsThenChars() {
         touchLongPressThenDragToEndPaddingTest(
-            endOffset = boundsInRoot.bottomRight + Offset(-1f, -1f),
+            endOffset = bottomEnd,
             endSelection = 12 to 29,
         )
     }
@@ -365,7 +527,7 @@
     @Test
     fun whenTouch_withLongPressInEndPadding_entersSelectionMode() {
         performTouchGesture {
-            longPress(boundsInRoot.topRight + Offset(-1f, 1f))
+            longPress(topEnd)
         }
 
         asserter.applyAndAssert {
@@ -375,7 +537,7 @@
         }
 
         // we want to test at least one drag that shouldn't affect selection as well
-        touchDragBy(Offset(-1f, 0f))
+        touchDragBy(Offset.Zero.nudge(START))
 
         asserter.assert()
 
@@ -391,7 +553,7 @@
     @Test
     fun whenTouch_withLongPressInEndPaddingThenDragToUpperEndPadding_selectsParagraphAndNewLine() {
         performTouchGesture {
-            longPress(boundsInRoot.centerRight + Offset(-1f, 0f))
+            longPress(centerEnd)
         }
 
         asserter.applyAndAssert {
@@ -400,7 +562,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.topRight + Offset(-1f, 1f))
+        touchDragTo(topEnd)
 
         asserter.applyAndAssert {
             selection = 18 to 0
@@ -408,7 +570,7 @@
         }
 
         // do it again for a regression where selection was only wrong the second time
-        touchDragTo(boundsInRoot.centerRight + Offset(-1f, 0f))
+        touchDragTo(centerEnd)
 
         asserter.applyAndAssert {
             selection = 18 to 23
@@ -416,7 +578,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.topRight + Offset(-1f, 1f))
+        touchDragTo(topEnd)
 
         asserter.applyAndAssert {
             selection = 18 to 0
@@ -435,7 +597,7 @@
     @Test
     fun whenTouch_withLongPressInEndPaddingThenDragToLowerEndPadding_selectsNewLineAndParagraph() {
         performTouchGesture {
-            longPress(boundsInRoot.centerRight + Offset(-1f, 0f))
+            longPress(centerEnd)
         }
 
         asserter.applyAndAssert {
@@ -444,7 +606,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.bottomRight + Offset(-1f, -1f))
+        touchDragTo(bottomEnd)
 
         asserter.applyAndAssert {
             selection = 18 to 29
@@ -452,7 +614,7 @@
         }
 
         // do it again for a regression where selection was only wrong the second time
-        touchDragTo(boundsInRoot.centerRight + Offset(-1f, 0f))
+        touchDragTo(centerEnd)
 
         asserter.applyAndAssert {
             selection = 18 to 23
@@ -460,7 +622,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.bottomRight + Offset(-1f, -1f))
+        touchDragTo(bottomEnd)
 
         asserter.applyAndAssert {
             selection = 18 to 29
@@ -479,7 +641,7 @@
     @Test
     fun whenTouch_withLongPressInEndPaddingOfFinalLine_entersSelectionMode() {
         performTouchGesture {
-            longPress(boundsInRoot.bottomRight + Offset(-1f, -1f))
+            longPress(bottomEnd)
         }
 
         asserter.applyAndAssert {
@@ -507,7 +669,7 @@
     @Test
     fun whenTouch_withLongPressInFinalLineEndPaddingThenDragToMidEndPadding_entersSelectionMode() {
         performTouchGesture {
-            longPress(boundsInRoot.bottomRight + Offset(-1f, -1f))
+            longPress(bottomEnd)
         }
 
         asserter.applyAndAssert {
@@ -516,7 +678,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.centerRight + Offset(-1f, 0f))
+        touchDragTo(centerEnd)
 
         asserter.applyAndAssert {
             selection = 24 to 18
@@ -524,7 +686,7 @@
         }
 
         // do it again for a regression where selection was only wrong the second time
-        touchDragTo(boundsInRoot.bottomRight + Offset(-1f, -1f))
+        touchDragTo(bottomEnd)
 
         asserter.applyAndAssert {
             selection = 24 to 29
@@ -532,7 +694,7 @@
             hapticsCount++
         }
 
-        touchDragTo(boundsInRoot.centerRight + Offset(-1f, 0f))
+        touchDragTo(centerEnd)
 
         asserter.applyAndAssert {
             selection = 24 to 18
@@ -552,7 +714,7 @@
     @Test
     fun whenTouch_withLongPressInEndPadding_selectsFinalWord() {
         performTouchGesture {
-            longPress(boundsInRoot.centerRight - Offset(1f, 0f))
+            longPress(centerEnd)
         }
 
         asserter.applyAndAssert {
@@ -843,7 +1005,7 @@
             }
         }
 
-        mouseDragTo(position = boundsInRoot.topRight + Offset(-1f, 1f))
+        mouseDragTo(position = topEnd)
 
         asserter.applyAndAssert {
             selection = endSelection
@@ -920,10 +1082,38 @@
         asserter.assert()
     }
 
+    // this is a collapsed selection in multi-text and a selection of just a newline in single text.
+    @Test
+    open fun whenMouseCollapsedSelectionAcrossLines_thenTouch_showUi() {
+        performMouseGesture {
+            moveTo(centerEnd)
+            press()
+        }
+
+        asserter.applyAndAssert {
+            selection = 23.collapsed
+        }
+
+        mouseDragTo(characterPosition(offset = 24))
+
+        asserter.applyAndAssert {
+            selection = 23 to 24
+        }
+
+        performTouchGesture {
+            enterTouchMode()
+        }
+
+        asserter.applyAndAssert {
+            selectionHandlesShown = true
+            textToolbarShown = true
+        }
+    }
+
     @Test
     fun whenMouse_thenTripleClickInEndPadding_selectsOnlyCurrentParagraph() {
         performMouseGesture {
-            moveTo(position = boundsInRoot.centerRight - Offset(1f, 0f))
+            moveTo(position = centerEnd)
             press()
             repeat(2) {
                 advanceEventTime()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionHandlesGesturesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionHandlesGesturesTest.kt
index 453be46..3436976 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionHandlesGesturesTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/TextSelectionHandlesGesturesTest.kt
@@ -57,9 +57,6 @@
     override val pointerAreaTag = "selectionContainer"
 
     private val textContent = "line1\nline2 text1 text2\nline3"
-    private val selectionContainerTestTag = "selectionContainer"
-    private val testTag = "testTag"
-
     private val selection = mutableStateOf<Selection?>(null)
 
     private lateinit var asserter: SelectionAsserter<Selection?>
@@ -67,7 +64,7 @@
     @Before
     fun setupAsserter() {
         performTouchGesture {
-            longClick(characterBox(13).center)
+            longClick(characterBox(13).centerLeft)
         }
 
         asserter = object : SelectionAsserter<Selection?>(
@@ -81,7 +78,11 @@
             override fun subAssert() {
                 Truth.assertAbout(SelectionSubject.withContent(textContent))
                     .that(getActual())
-                    .hasSelection(selection)
+                    .hasSelection(
+                        expected = selection,
+                        startTextDirection = startLayoutDirection,
+                        endTextDirection = endLayoutDirection,
+                    )
             }
         }.apply {
             selection = 12 to 17
@@ -96,9 +97,7 @@
         SelectionContainer(
             selection = selection.value,
             onSelectionChange = { selection.value = it },
-            modifier = Modifier
-                .fillMaxSize()
-                .testTag(selectionContainerTestTag)
+            modifier = Modifier.fillMaxSize()
         ) {
             BasicText(
                 text = textContent,
@@ -109,7 +108,7 @@
                 modifier = Modifier
                     .fillMaxWidth()
                     .wrapContentHeight()
-                    .testTag(testTag),
+                    .testTag(pointerAreaTag),
             )
         }
     }
@@ -173,8 +172,7 @@
     }
 
     private fun characterBox(offset: Int): Rect {
-        val nodePosition = rule.onNodeWithTag(testTag).fetchSemanticsNode().positionInRoot
-        val textLayoutResult = rule.onNodeWithTag(testTag).fetchTextLayoutResult()
-        return textLayoutResult.getBoundingBox(offset).translate(nodePosition)
+        val textLayoutResult = rule.onNodeWithTag(pointerAreaTag).fetchTextLayoutResult()
+        return textLayoutResult.getBoundingBox(offset)
     }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/MultiTextSelectionTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/MultiTextSelectionTestUtils.kt
index e6b00b1..8f1cebc 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/MultiTextSelectionTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/MultiTextSelectionTestUtils.kt
@@ -18,7 +18,7 @@
 
 import androidx.compose.foundation.text.selection.Selection
 import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.style.ResolvedTextDirection.Ltr
+import androidx.compose.ui.text.style.ResolvedTextDirection
 import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.Subject
@@ -44,7 +44,11 @@
             }
     }
 
-    fun hasSelection(expected: TextRange?) {
+    fun hasSelection(
+        expected: TextRange?,
+        startTextDirection: ResolvedTextDirection,
+        endTextDirection: ResolvedTextDirection,
+    ) {
         if (expected == null) {
             Truth.assertThat(subject).isNull()
             return
@@ -57,9 +61,12 @@
         val endSelectableId = textContentIndices.offsetToSelectableId(expected.end) + 1
         val endOffset = textContentIndices.offsetToLocalOffset(expected.end)
 
+        val startAnchor =
+            Selection.AnchorInfo(startTextDirection, startOffset, startSelectableId.toLong())
+        val endAnchor = Selection.AnchorInfo(endTextDirection, endOffset, endSelectableId.toLong())
         val expectedSelection = Selection(
-            start = Selection.AnchorInfo(Ltr, startOffset, startSelectableId.toLong()),
-            end = Selection.AnchorInfo(Ltr, endOffset, endSelectableId.toLong()),
+            start = startAnchor,
+            end = endAnchor,
             handlesCrossed = startSelectableId > endSelectableId ||
                 (startSelectableId == endSelectableId && startOffset > endOffset),
         )
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/SingleTextSelectionTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/SingleTextSelectionTestUtils.kt
index 187c080..2bd69a7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/SingleTextSelectionTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/SingleTextSelectionTestUtils.kt
@@ -43,7 +43,11 @@
             }
     }
 
-    fun hasSelection(expected: TextRange?) {
+    fun hasSelection(
+        expected: TextRange?,
+        startTextDirection: ResolvedTextDirection,
+        endTextDirection: ResolvedTextDirection,
+    ) {
         if (expected == null) {
             Truth.assertThat(subject).isNull()
             return
@@ -52,8 +56,8 @@
         check("selection").that(subject).isNotNull()
         subject!! // smart cast to non-nullable
 
-        val startHandle = Selection.AnchorInfo(ResolvedTextDirection.Ltr, expected.start, 1)
-        val endHandle = Selection.AnchorInfo(ResolvedTextDirection.Ltr, expected.end, 1)
+        val startHandle = Selection.AnchorInfo(startTextDirection, expected.start, 1)
+        val endHandle = Selection.AnchorInfo(endTextDirection, expected.end, 1)
         val expectedSelection = Selection(
             start = startHandle,
             end = endHandle,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextSelectionTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextSelectionTestUtils.kt
index bb2e1a5..a2b89c4 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextSelectionTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/gestures/util/TextSelectionTestUtils.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.test.TouchInjectionScope
 import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.style.ResolvedTextDirection
 import com.google.common.truth.Truth
 
 private fun ComposeTestRule.assertSelectionHandlesShown(shown: Boolean) {
@@ -85,6 +86,8 @@
     var textToolbarShown = false
     var magnifierShown = false
     var hapticsCount = 0
+    var startLayoutDirection = ResolvedTextDirection.Ltr
+    var endLayoutDirection = ResolvedTextDirection.Ltr
 
     fun assert() {
         subAssert()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt
index 59d1a2c..0def91c 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicSecureTextFieldTest.kt
@@ -38,9 +38,11 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performSemanticsAction
@@ -49,15 +51,17 @@
 import androidx.compose.ui.test.performTextReplacement
 import androidx.compose.ui.test.requestFocus
 import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class BasicSecureTextFieldTest {
@@ -69,6 +73,9 @@
         mainClock.autoAdvance = false
     }
 
+    @get:Rule
+    val inputMethodInterceptor = InputMethodInterceptorRule(rule)
+
     private val Tag = "BasicSecureTextField"
 
     @Test
@@ -368,4 +375,303 @@
             assertThat(cutOptionAvailable).isFalse()
         }
     }
+
+    @Test
+    fun stringValue_updatesFieldText_whenTextChangedFromCode_whileUnfocused() {
+        var text by mutableStateOf("hello")
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.runOnIdle {
+            text = "world"
+        }
+        // Auto-advance is disabled.
+        rule.mainClock.advanceTimeByFrame()
+
+        assertThat(
+            rule.onNodeWithTag(Tag).fetchSemanticsNode().config[SemanticsProperties.EditableText]
+                .text
+        ).isEqualTo("world")
+    }
+
+    @Test
+    fun textFieldValue_updatesFieldText_whenTextChangedFromCode_whileUnfocused() {
+        var text by mutableStateOf(TextFieldValue("hello"))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.runOnIdle {
+            text = text.copy(text = "world")
+        }
+        // Auto-advance is disabled.
+        rule.mainClock.advanceTimeByFrame()
+
+        assertThat(
+            rule.onNodeWithTag(Tag).fetchSemanticsNode().config[SemanticsProperties.EditableText]
+                .text
+        ).isEqualTo("world")
+    }
+
+    @Test
+    fun textFieldValue_updatesFieldSelection_whenSelectionChangedFromCode_whileUnfocused() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.runOnIdle {
+            text = text.copy(selection = TextRange(2))
+        }
+        // Auto-advance is disabled.
+        rule.mainClock.advanceTimeByFrame()
+
+        assertTextSelection(TextRange(2))
+    }
+
+    @Test
+    fun stringValue_doesNotUpdateField_whenTextChangedFromCode_whileFocused() {
+        var text by mutableStateOf("hello")
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+
+        rule.runOnIdle {
+            text = "world"
+        }
+
+        rule.onNodeWithTag(Tag).assertTextEquals("hello")
+    }
+
+    @Test
+    fun textFieldValue_doesNotUpdateField_whenTextChangedFromCode_whileFocused() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+
+        rule.runOnIdle {
+            text = TextFieldValue(text = "world", selection = TextRange(2))
+        }
+
+        rule.onNodeWithTag(Tag).assertTextEquals("hello")
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_onFocus() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        assertThat(onValueChangedCount).isEqualTo(0)
+
+        requestFocus(Tag)
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_whenOnlySelectionChanged() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+        assertThat(onValueChangedCount).isEqualTo(0)
+
+        // Act: wiggle the cursor around a bit.
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(0))
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(5))
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_whenOnlyCompositionChanged() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+        assertThat(onValueChangedCount).isEqualTo(0)
+
+        // Act: wiggle the composition around a bit
+        inputMethodInterceptor.withInputConnection { setComposingRegion(0, 0) }
+        inputMethodInterceptor.withInputConnection { setComposingRegion(3, 5) }
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_whenTextChangedFromCode_whileUnfocused() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        assertThat(onValueChangedCount).isEqualTo(0)
+
+        rule.runOnIdle {
+            text = "hello"
+        }
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun stringValue_doesNotInvokeCallback_whenTextChangedFromCode_whileFocused() {
+        var text by mutableStateOf("")
+        var onValueChangedCount = 0
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = {
+                    text = it
+                    onValueChangedCount++
+                },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        assertThat(onValueChangedCount).isEqualTo(0)
+        requestFocus(Tag)
+
+        rule.runOnIdle {
+            text = "hello"
+        }
+
+        rule.runOnIdle {
+            assertThat(onValueChangedCount).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun textFieldValue_usesInitialSelectionFromValue() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(2)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertTextSelection(TextRange(2))
+    }
+
+    @Test
+    fun textFieldValue_reportsSelectionChangesInCallback() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(2))
+
+        rule.runOnIdle {
+            assertThat(text.selection).isEqualTo(TextRange(2))
+        }
+    }
+
+    @Test
+    fun textFieldValue_reportsCompositionChangesInCallback() {
+        var text by mutableStateOf(TextFieldValue("hello", selection = TextRange(1)))
+        rule.setContent {
+            BasicSecureTextField(
+                value = text,
+                onValueChange = { text = it },
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+
+        inputMethodInterceptor.withInputConnection { setComposingRegion(0, 0) }
+        rule.runOnIdle {
+            assertWithMessage(
+                "After setting composing region to 0, 0, TextFieldState's composition is:"
+            ).that(text.composition).isNull()
+        }
+
+        inputMethodInterceptor.withInputConnection { setComposingRegion(1, 4) }
+        rule.runOnIdle {
+            assertWithMessage(
+                "After setting composing region to 1, 4, TextFieldState's composition is:"
+            ).that(text.composition).isEqualTo(TextRange(1, 4))
+        }
+    }
+
+    private fun requestFocus(tag: String) =
+        rule.onNodeWithTag(tag).requestFocus()
+
+    private fun assertTextSelection(expected: TextRange) {
+        val selection = rule.onNodeWithTag(Tag).fetchSemanticsNode()
+            .config.getOrNull(SemanticsProperties.TextSelectionRange)
+        assertWithMessage("Expected selection to be $expected")
+            .that(selection).isEqualTo(expected)
+    }
 }
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 30d025e..3370e21 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
@@ -537,9 +537,23 @@
             val finalDistance = when (sign(currentVelocity)) {
                 0f -> {
                     if (offsetFromSnappedPositionOverflow.absoluteValue > snapPositionalThreshold) {
+                        // If we crossed the threshold, go to the next bound
                         if (isForward) upperBoundOffset else lowerBoundOffset
                     } else {
-                        if (isForward) lowerBoundOffset else upperBoundOffset
+                        // if we haven't crossed the threshold. but scrolled minimally, we should
+                        // bound to the previous bound
+                        if (abs(pagerState.currentPageOffsetFraction) >=
+                            abs(pagerState.positionThresholdFraction)
+                        ) {
+                            if (isForward) lowerBoundOffset else upperBoundOffset
+                        } else {
+                            // if we haven't scrolled minimally, settle for the closest bound
+                            if (lowerBoundOffset.absoluteValue < upperBoundOffset.absoluteValue) {
+                                lowerBoundOffset
+                            } else {
+                                upperBoundOffset
+                            }
+                        }
                     }
                 }
 
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 8efdab9..b662c3d 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
@@ -233,7 +233,7 @@
      * How far the current page needs to scroll so the target page is considered to be the next
      * page.
      */
-    private val positionThresholdFraction: Float
+    internal val positionThresholdFraction: Float
         get() = with(density) {
             val minThreshold = minOf(DefaultPositionThreshold.toPx(), pageSize / 2f)
             minThreshold / pageSize.toFloat()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index 95bc432..e5219f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -29,6 +29,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.ColorProducer
@@ -95,8 +96,13 @@
     val selectionRegistrar = LocalSelectionRegistrar.current
     val selectionController = if (selectionRegistrar != null) {
         val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
-        remember(selectionRegistrar, backgroundSelectionColor) {
+        val selectableId =
+            rememberSaveable(selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) {
+                selectionRegistrar.nextSelectableId()
+            }
+        remember(selectableId, selectionRegistrar, backgroundSelectionColor) {
             SelectionController(
+                selectableId,
                 selectionRegistrar,
                 backgroundSelectionColor
             )
@@ -184,8 +190,13 @@
     val selectionRegistrar = LocalSelectionRegistrar.current
     val selectionController = if (selectionRegistrar != null) {
         val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
-        remember(selectionRegistrar, backgroundSelectionColor) {
+        val selectableId =
+            rememberSaveable(selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) {
+                selectionRegistrar.nextSelectableId()
+            }
+        remember(selectableId, selectionRegistrar, backgroundSelectionColor) {
             SelectionController(
+                selectableId,
                 selectionRegistrar,
                 backgroundSelectionColor
             )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
index e54ae82..57adcad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -55,7 +55,7 @@
     minLines: Int = DefaultMinLines,
     placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
     onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
-    private val selectionController: SelectionController? = null,
+    private var selectionController: SelectionController? = null,
     overrideColor: ColorProducer? = null
 ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode {
 
@@ -147,6 +147,7 @@
                 selectionController = selectionController
             ),
         )
+        this.selectionController = selectionController
         // we always relayout when we're selectable
         invalidateMeasurement()
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectionController.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectionController.kt
index 34a9f2c9..ff34ae3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectionController.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectionController.kt
@@ -68,13 +68,13 @@
  */
 // This is _basically_ a Modifier.Node but moved into remember because we need to do pointerInput
 internal class SelectionController(
+    private val selectableId: Long,
     private val selectionRegistrar: SelectionRegistrar,
     private val backgroundSelectionColor: Color,
     // TODO: Move these into Modifer.element eventually
     private var params: StaticTextSelectionParams = StaticTextSelectionParams.Empty
 ) : RememberObserver {
     private var selectable: Selectable? = null
-    private val selectableId = selectionRegistrar.nextSelectableId()
 
     val modifier: Modifier = selectionRegistrar
         .makeSelectionModifier(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index 3061137..8fbd187 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -18,11 +18,11 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.isUnspecified
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.unit.toSize
 import kotlin.math.max
 
 internal class MultiWidgetSelectionDelegate(
@@ -68,54 +68,46 @@
             return _previousLastVisibleOffset
         }
 
-    override fun updateSelection(
-        startHandlePosition: Offset,
-        endHandlePosition: Offset,
-        previousHandlePosition: Offset?,
-        isStartHandle: Boolean,
-        containerLayoutCoordinates: LayoutCoordinates,
-        adjustment: SelectionAdjustment,
-        previousSelection: Selection?
-    ): Pair<Selection?, Boolean> {
-        require(
-            previousSelection == null || (
-                selectableId == previousSelection.start.selectableId &&
-                    selectableId == previousSelection.end.selectableId
-                )
-        ) {
-            "The given previousSelection doesn't belong to this selectable."
+    override fun appendSelectableInfoToBuilder(builder: SelectionLayoutBuilder) {
+        val layoutCoordinates = getLayoutCoordinates() ?: return
+        val textLayoutResult = layoutResultCallback() ?: return
+
+        val relativePosition =
+            builder.containerCoordinates.localPositionOf(layoutCoordinates, Offset.Zero)
+        val localStartPosition = builder.startHandlePosition - relativePosition
+        val localEndPosition = builder.endHandlePosition - relativePosition
+        val localPreviousHandlePosition = if (builder.previousHandlePosition.isUnspecified) {
+            Offset.Unspecified
+        } else {
+            builder.previousHandlePosition - relativePosition
         }
-        val layoutCoordinates = getLayoutCoordinates() ?: return Pair(null, false)
-        val textLayoutResult = layoutResultCallback() ?: return Pair(null, false)
 
-        val relativePosition = containerLayoutCoordinates.localPositionOf(
-            layoutCoordinates, Offset.Zero
-        )
-        val localStartPosition = startHandlePosition - relativePosition
-        val localEndPosition = endHandlePosition - relativePosition
-        val localPreviousHandlePosition = previousHandlePosition?.let { it - relativePosition }
-
-        return getTextSelectionInfo(
+        builder.appendSelectableInfo(
             textLayoutResult = textLayoutResult,
-            startHandlePosition = localStartPosition,
-            endHandlePosition = localEndPosition,
+            startPosition = localStartPosition,
+            endPosition = localEndPosition,
             previousHandlePosition = localPreviousHandlePosition,
             selectableId = selectableId,
-            adjustment = adjustment,
-            previousSelection = previousSelection,
-            isStartHandle = isStartHandle
         )
     }
 
     override fun getSelectAllSelection(): Selection? {
         val textLayoutResult = layoutResultCallback() ?: return null
-        val newSelectionRange = TextRange(0, textLayoutResult.layoutInput.text.length)
+        val start = 0
+        val end = textLayoutResult.layoutInput.text.length
 
-        return getAssembledSelectionInfo(
-            newSelectionRange = newSelectionRange,
-            handlesCrossed = false,
-            selectableId = selectableId,
-            textLayoutResult = textLayoutResult
+        return Selection(
+            start = Selection.AnchorInfo(
+                direction = textLayoutResult.getBidiRunDirection(start),
+                offset = start,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = textLayoutResult.getBidiRunDirection(max(end - 1, 0)),
+                offset = end,
+                selectableId = selectableId
+            ),
+            handlesCrossed = false
         )
     }
 
@@ -160,6 +152,26 @@
         )
     }
 
+    override fun getLineLeft(offset: Int): Float {
+        val textLayoutResult = layoutResultCallback() ?: return -1f
+        val line = textLayoutResult.getLineForOffset(offset)
+        return textLayoutResult.getLineLeft(line)
+    }
+
+    override fun getLineRight(offset: Int): Float {
+        val textLayoutResult = layoutResultCallback() ?: return -1f
+        val line = textLayoutResult.getLineForOffset(offset)
+        return textLayoutResult.getLineRight(line)
+    }
+
+    override fun getCenterYForOffset(offset: Int): Float {
+        val textLayoutResult = layoutResultCallback() ?: return -1f
+        val line = textLayoutResult.getLineForOffset(offset)
+        val top = textLayoutResult.getLineTop(line)
+        val bottom = textLayoutResult.getLineBottom(line)
+        return ((bottom - top) / 2) + top
+    }
+
     override fun getRangeOfLineContaining(offset: Int): TextRange {
         val textLayoutResult = layoutResultCallback() ?: return TextRange.Zero
         val visibleTextLength = textLayoutResult.lastVisibleOffset
@@ -178,32 +190,23 @@
 }
 
 /**
- * Return information about the current selection in the Text.
+ * Appends a [SelectableInfo] to this [SelectionLayoutBuilder].
  *
- * @param textLayoutResult a result of the text layout.
- * @param startHandlePosition The new positions of the moving selection handle.
- * @param previousHandlePosition The old position of the moving selection handle since the last update.
- * @param endHandlePosition the position of the selection handle that is not moving.
- * @param selectableId the id of this [Selectable].
- * @param adjustment the [SelectionAdjustment] used to process the raw selection range.
- * @param previousSelection the previous text selection.
- * @param isStartHandle whether the moving selection is the start selection handle.
- *
- * @return a pair consistent of updated [Selection] and a boolean representing whether the
- * movement is consumed.
+ * @param textLayoutResult the [TextLayoutResult] for the selectable
+ * @param startPosition the position of the start handle if not being draggedor the drag position if
+ *                      it is
+ * @param endPosition  the position of the end handle if not being dragged or the drag position if
+ *                     it is
+ * @param previousHandlePosition the position of the previous handle
+ * @param selectableId the selectableId for the selectable
  */
-internal fun getTextSelectionInfo(
+internal fun SelectionLayoutBuilder.appendSelectableInfo(
     textLayoutResult: TextLayoutResult,
-    startHandlePosition: Offset,
-    endHandlePosition: Offset,
-    previousHandlePosition: Offset?,
+    startPosition: Offset,
+    endPosition: Offset,
+    previousHandlePosition: Offset,
     selectableId: Long,
-    adjustment: SelectionAdjustment,
-    previousSelection: Selection? = null,
-    isStartHandle: Boolean = true
-): Pair<Selection?, Boolean> {
-
-    val currentHandlePosition = if (isStartHandle) startHandlePosition else endHandlePosition
+) {
     val bounds = Rect(
         0.0f,
         0.0f,
@@ -212,150 +215,71 @@
     )
 
     val isSelected =
-        SelectionMode.Vertical.isSelected(bounds, startHandlePosition, endHandlePosition)
+        SelectionMode.Vertical.isSelected(bounds, startPosition, endPosition)
 
     if (!isSelected) {
-        return Pair(null, false)
+        return
     }
 
-    // check if going horizontally on the same line/Text and getting out of horizontal bounds.
-    // if so reject the change.
-    if (isMovingOutOfBoundsOnTheSameLineInCurrentText(
-            previousHandlePosition,
-            currentHandlePosition,
-            textLayoutResult
-        )
-    ) {
-        return Pair(previousSelection, false)
-    }
-
-    val rawStartHandleOffset = getOffsetForPosition(textLayoutResult, bounds, startHandlePosition)
-    val rawEndHandleOffset = getOffsetForPosition(textLayoutResult, bounds, endHandlePosition)
-    val rawCurrentHandleOffset = if (isStartHandle) rawStartHandleOffset else rawEndHandleOffset
-    val rawPreviousHandleOffset = previousHandlePosition?.let {
-        getOffsetForPosition(textLayoutResult, bounds, it)
-    } ?: -1
-
-    val adjustedTextRange = adjustment.adjust(
-        textLayoutResult = textLayoutResult,
-        newRawSelectionRange = TextRange(rawStartHandleOffset, rawEndHandleOffset),
-        previousHandleOffset = rawPreviousHandleOffset,
-        isStartHandle = isStartHandle,
-        previousSelectionRange = previousSelection?.toTextRange()
-    )
-
-    // Edge case where it isn't clear whether our selection should be reversed,
-    // Our offsets (previous, start, and end) are the same.
-    // If our current position is in bounds,
-    // but our previous position out of bounds and forwards,
-    // then we want to make sure that our selection is reversed
-    // because it is a backwards selection
-    val shouldReverseRange = rawCurrentHandleOffset == rawPreviousHandleOffset &&
-        rawStartHandleOffset == rawEndHandleOffset &&
-        previousHandlePosition != null &&
-        SelectionMode.Vertical.compare(currentHandlePosition, bounds) == 0 &&
-        SelectionMode.Vertical.compare(previousHandlePosition, bounds) > 0
-
-    val selectionRange = adjustedTextRange.run {
-        if (shouldReverseRange) TextRange(max, min) else this
-    }
-
-    val newSelection = getAssembledSelectionInfo(
-        newSelectionRange = selectionRange,
-        handlesCrossed = selectionRange.reversed,
-        selectableId = selectableId,
-        textLayoutResult = textLayoutResult
-    )
-
-    // Determine whether the movement is consumed by this Selectable.
-    // If the selection has  changed, the movement is consumed.
-    // And there are also cases where the selection stays the same but selection handle raw
-    // offset has changed.(Usually this happen because of adjustment like SelectionAdjustment.Word)
-    // In this case we also consider the movement being consumed.
-    val selectionUpdated = newSelection != previousSelection
-    val handleUpdated = rawCurrentHandleOffset != rawPreviousHandleOffset
-    val consumed = handleUpdated || selectionUpdated
-    return Pair(newSelection, consumed)
-}
-
-/**
- * Returns true if the handle is moving horizontally on the same line/text and getting out of
- * horizontal bounds on left or right.
- */
-private fun isMovingOutOfBoundsOnTheSameLineInCurrentText(
-    previousHandlePosition: Offset?,
-    currentHandlePosition: Offset,
-    textLayoutResult: TextLayoutResult
-): Boolean {
-    if (previousHandlePosition == null) {
-        return false
-    }
-
-    val bounds = Rect(Offset.Zero, textLayoutResult.size.toSize())
-    if (
-        !bounds.containsInclusive(previousHandlePosition) ||
-        !bounds.containsInclusive(currentHandlePosition)
-    ) {
-        return false
-    }
-
-    val previousHandleLine = textLayoutResult.getLineForVerticalPosition(previousHandlePosition.y)
-    val currentHandleLine = textLayoutResult.getLineForVerticalPosition(currentHandlePosition.y)
-    if (currentHandleLine != previousHandleLine) return false
-
-    val lineRight = textLayoutResult.getLineRight(currentHandleLine)
-    val lineLeft = textLayoutResult.getLineLeft(currentHandleLine)
-
-    // When x is equal to the line sides,
-    // it still can trigger a selection change that we want to avoid
-    // (selecting the whitespace at the ends),
-    // so return true for those as well.
-    return currentHandlePosition.x <= lineLeft || lineRight <= currentHandlePosition.x
-}
-
-private fun getOffsetForPosition(
-    textLayoutResult: TextLayoutResult,
-    bounds: Rect,
-    position: Offset
-): Int {
-    val length = textLayoutResult.layoutInput.text.length
-    return if (bounds.contains(position)) {
-        textLayoutResult.getOffsetForPosition(position).coerceIn(0, length)
+    val textLength = textLayoutResult.layoutInput.text.length
+    val rawStartHandleOffset: Int
+    val rawEndHandleOffset: Int
+    if (isStartHandle) {
+        rawStartHandleOffset = getOffsetForPosition(startPosition, textLayoutResult)
+        rawEndHandleOffset = previousSelection?.end
+            ?.getPreviousAdjustedOffset(selectableIdOrderingComparator, selectableId, textLength)
+            ?: getOffsetForPosition(endPosition, textLayoutResult)
     } else {
-        val value = SelectionMode.Vertical.compare(position, bounds)
-        if (value < 0) 0 else length
+        rawStartHandleOffset = previousSelection?.start
+            ?.getPreviousAdjustedOffset(selectableIdOrderingComparator, selectableId, textLength)
+            ?: getOffsetForPosition(startPosition, textLayoutResult)
+        rawEndHandleOffset = getOffsetForPosition(endPosition, textLayoutResult)
+    }
+
+    val rawPreviousHandleOffset = if (previousHandlePosition.isUnspecified) -1 else {
+        getOffsetForPosition(previousHandlePosition, textLayoutResult)
+    }
+
+    val startHandleDirection = getDirection(startPosition, bounds)
+    val endHandleDirection = getDirection(endPosition, bounds)
+
+    appendInfo(
+        selectableId = selectableId,
+        rawStartHandleOffset = rawStartHandleOffset,
+        startHandleDirection = startHandleDirection,
+        rawEndHandleOffset = rawEndHandleOffset,
+        endHandleDirection = endHandleDirection,
+        rawPreviousHandleOffset = rawPreviousHandleOffset,
+        textLayoutResult = textLayoutResult,
+    )
+}
+
+private fun Selection.AnchorInfo.getPreviousAdjustedOffset(
+    selectableIdOrderingComparator: Comparator<Long>,
+    currentSelectableId: Long,
+    currentTextLength: Int
+): Int {
+    val compareResult = selectableIdOrderingComparator.compare(
+        this.selectableId,
+        currentSelectableId
+    )
+
+    return when {
+        compareResult < 0 -> 0
+        compareResult > 0 -> currentTextLength
+        else -> offset
     }
 }
 
-/**
- * [Selection] contains a lot of parameters. It looks more clean to assemble an object of this
- * class in a separate method.
- *
- * @param newSelectionRange the final new selection text range.
- * @param handlesCrossed true if the selection handles are crossed
- * @param selectableId the id of the current [Selectable] for which the [Selection] is being
- * calculated
- * @param textLayoutResult a result of the text layout.
- *
- * @return an assembled object of [Selection] using the offered selection info.
- */
-private fun getAssembledSelectionInfo(
-    newSelectionRange: TextRange,
-    handlesCrossed: Boolean,
-    selectableId: Long,
-    textLayoutResult: TextLayoutResult
-): Selection {
-    return Selection(
-        start = Selection.AnchorInfo(
-            direction = textLayoutResult.getBidiRunDirection(newSelectionRange.start),
-            offset = newSelectionRange.start,
-            selectableId = selectableId
-        ),
-        end = Selection.AnchorInfo(
-            direction = textLayoutResult.getBidiRunDirection(max(newSelectionRange.end - 1, 0)),
-            offset = newSelectionRange.end,
-            selectableId = selectableId
-        ),
-        handlesCrossed = handlesCrossed
-    )
+private fun getDirection(position: Offset, bounds: Rect): Direction = when {
+    position.y < bounds.top -> Direction.BEFORE
+    position.y > bounds.bottom -> Direction.AFTER
+    else -> Direction.ON
+}
+
+// map offsets above/below the text to 0/length respectively
+private fun getOffsetForPosition(position: Offset, textLayoutResult: TextLayoutResult): Int = when {
+    position.y <= 0f -> 0
+    position.y >= textLayoutResult.multiParagraph.height -> textLayoutResult.layoutInput.text.length
+    else -> textLayoutResult.getOffsetForPosition(position)
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
index 0d227f8..4d87425 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
@@ -41,33 +41,10 @@
     val selectableId: Long
 
     /**
-     * Updates the [Selection] information after a selection handle being moved. This method is
-     * expected to be called consecutively during the selection handle position update.
-     *
-     * @param startHandlePosition graphical position of the start selection handle
-     * @param endHandlePosition graphical position of the end selection handle
-     * @param previousHandlePosition the previous position of the moving selection handle
-     * @param containerLayoutCoordinates [LayoutCoordinates] of the composable
-     * @param adjustment [Selection] range is adjusted according to this param
-     * @param previousSelection previous selection result on this [Selectable]
-     * @param isStartHandle whether the moving selection handle is the start selection handle
-     *
-     * @throws IllegalStateException when the given [previousSelection] doesn't belong to this
-     * selectable. In other words, one of the [Selection.AnchorInfo] in the given
-     * [previousSelection] has a selectableId that doesn't match to the [selectableId] of this
-     * selectable.
-     * @return a pair consisting of the updated [Selection] and a boolean value representing
-     * whether the movement is consumed.
+     * A function which adds [SelectableInfo] representing this [Selectable]
+     * to the [SelectionLayoutBuilder].
      */
-    fun updateSelection(
-        startHandlePosition: Offset,
-        endHandlePosition: Offset,
-        previousHandlePosition: Offset?,
-        isStartHandle: Boolean = true,
-        containerLayoutCoordinates: LayoutCoordinates,
-        adjustment: SelectionAdjustment,
-        previousSelection: Selection? = null
-    ): Pair<Selection?, Boolean>
+    fun appendSelectableInfoToBuilder(builder: SelectionLayoutBuilder)
 
     /**
      * Returns selectAll [Selection] information for a selectable composable. If no selection can be
@@ -117,6 +94,33 @@
     fun getBoundingBox(offset: Int): Rect
 
     /**
+     * Returns the left x coordinate of the line for the given offset.
+     *
+     * @param offset a character offset
+     * @return the line left x coordinate for the given offset
+     */
+    fun getLineLeft(offset: Int): Float
+
+    /**
+     * Returns the right x coordinate of the line for the given offset.
+     *
+     * @param offset a character offset
+     * @return the line right x coordinate for the given offset
+     */
+    fun getLineRight(offset: Int): Float
+
+    /**
+     * Returns the center y coordinate of the line on which the specified text offset appears.
+     *
+     * If you ask for a position before 0, you get the center of the first line;
+     * if you ask for a position beyond the end of the text, you get the center of the last line.
+     *
+     * @param offset a character offset
+     * @return the line center y coordinate of the line containing [offset]
+     */
+    fun getCenterYForOffset(offset: Int): Float
+
+    /**
      * Return the offsets of the start and end of the line containing [offset], or [TextRange.Zero]
      * if the selectable is empty. These offsets are in the same "coordinate space" as
      * [getBoundingBox], and despite being returned in a [TextRange], may not refer to offsets in
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustment.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustment.kt
index c35e650..c247123 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustment.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustment.kt
@@ -19,7 +19,6 @@
 import androidx.compose.foundation.text.findFollowingBreak
 import androidx.compose.foundation.text.findPrecedingBreak
 import androidx.compose.foundation.text.getParagraphBoundary
-import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
 
 /**
@@ -28,78 +27,34 @@
  * without adjustments. With a mouse, double-click selects by words and triple-clicks by paragraph.
  * @see [SelectionRegistrar.notifySelectionUpdate]
  */
-internal interface SelectionAdjustment {
+internal fun interface SelectionAdjustment {
 
     /**
      * The callback function that is called once a new selection arrives, the return value of
-     * this function will be the final selection range on the corresponding [Selectable].
-     *
-     * @param textLayoutResult the [TextLayoutResult] of the involved [Selectable].
-     * @param newRawSelectionRange the new selection range computed from the selection handle
-     * position on screen.
-     * @param previousHandleOffset the previous offset of the moving handle. When isStartHandle is
-     * true, it's the previous offset of the start handle before the movement, and vice versa.
-     * When there isn't a valid previousHandleOffset, previousHandleOffset should be -1.
-     * Necessary for adjustment to tell whether selection is expanding or shrinking.
-     * @param isStartHandle whether the moving handle is the start handle.
-     * @param previousSelectionRange the previous selection range, or the selection range to be
-     * updated.
+     * this function will be the final adjusted [Selection].
      */
-    fun adjust(
-        textLayoutResult: TextLayoutResult,
-        newRawSelectionRange: TextRange,
-        previousHandleOffset: Int,
-        isStartHandle: Boolean,
-        previousSelectionRange: TextRange?
-    ): TextRange
+    fun adjust(layout: SelectionLayout): Selection
 
     companion object {
         /**
          * The selection adjustment that does nothing and directly return the input raw
          * selection range.
          */
-        val None = object : SelectionAdjustment {
-            override fun adjust(
-                textLayoutResult: TextLayoutResult,
-                newRawSelectionRange: TextRange,
-                previousHandleOffset: Int,
-                isStartHandle: Boolean,
-                previousSelectionRange: TextRange?
-            ): TextRange = newRawSelectionRange
+        val None = SelectionAdjustment { layout ->
+            Selection(
+                start = layout.startInfo.anchorForOffset(layout.startInfo.rawStartHandleOffset),
+                end = layout.endInfo.anchorForOffset(layout.endInfo.rawEndHandleOffset),
+                handlesCrossed = layout.crossStatus == CrossStatus.CROSSED
+            )
         }
 
         /**
          * The character based selection. It normally won't change the raw selection range except
-         * when the input raw selection range is collapsed. In this case, it will always make
-         * sure at least one character is selected.
-         * When the given raw selection range is collapsed:
-         * a) it will always try to adjust the changing selection boundary(base on the value of
-         * isStartHandle) and makes sure the other boundary remains the same after the adjustment
-         * b) if the previous selection range is reversed, it will try to make the adjusted
-         * selection range reversed as well, and vice versa.
+         * when the input raw selection range is collapsed. In this case, it will almost
+         * always make sure at least one character is selected.
          */
-        val Character = object : SelectionAdjustment {
-            override fun adjust(
-                textLayoutResult: TextLayoutResult,
-                newRawSelectionRange: TextRange,
-                previousHandleOffset: Int,
-                isStartHandle: Boolean,
-                previousSelectionRange: TextRange?
-            ): TextRange {
-                return if (newRawSelectionRange.collapsed) {
-                    // If there isn't any selection before, we assume handles are not crossed.
-                    val previousHandlesCrossed = previousSelectionRange?.reversed ?: false
-                    ensureAtLeastOneChar(
-                        text = textLayoutResult.layoutInput.text.text,
-                        offset = newRawSelectionRange.start,
-                        lastOffset = textLayoutResult.layoutInput.text.lastIndex,
-                        isStartHandle = isStartHandle,
-                        previousHandlesCrossed = previousHandlesCrossed
-                    )
-                } else {
-                    newRawSelectionRange
-                }
-            }
+        val Character = SelectionAdjustment { layout ->
+            None.adjust(layout).ensureAtLeastOneChar(layout)
         }
 
         /**
@@ -108,19 +63,9 @@
          * selection range to the closest word boundary. If the raw selection is reversed, it
          * will always return a reversed selection, and vice versa.
          */
-        val Word = object : SelectionAdjustment {
-            override fun adjust(
-                textLayoutResult: TextLayoutResult,
-                newRawSelectionRange: TextRange,
-                previousHandleOffset: Int,
-                isStartHandle: Boolean,
-                previousSelectionRange: TextRange?
-            ): TextRange {
-                return adjustByBoundary(
-                    textLayoutResult = textLayoutResult,
-                    newRawSelection = newRawSelectionRange,
-                    boundaryFun = textLayoutResult::getWordBoundary
-                )
+        val Word = SelectionAdjustment { layout ->
+            adjustToBoundaries(layout) {
+                textLayoutResult.getWordBoundary(it)
             }
         }
 
@@ -130,51 +75,18 @@
          * raw input selection range to the closest paragraph boundary. If the raw selection is
          * reversed, it will always return a reversed selection, and vice versa.
          */
-        val Paragraph = object : SelectionAdjustment {
-            override fun adjust(
-                textLayoutResult: TextLayoutResult,
-                newRawSelectionRange: TextRange,
-                previousHandleOffset: Int,
-                isStartHandle: Boolean,
-                previousSelectionRange: TextRange?
-            ): TextRange {
-                val boundaryFun = textLayoutResult.layoutInput.text::getParagraphBoundary
-                return adjustByBoundary(
-                    textLayoutResult = textLayoutResult,
-                    newRawSelection = newRawSelectionRange,
-                    boundaryFun = boundaryFun
-                )
+        val Paragraph = SelectionAdjustment { layout ->
+            adjustToBoundaries(layout) {
+                inputText.getParagraphBoundary(it)
             }
         }
 
-        private fun adjustByBoundary(
-            textLayoutResult: TextLayoutResult,
-            newRawSelection: TextRange,
-            boundaryFun: (Int) -> TextRange
-        ): TextRange {
-            if (textLayoutResult.layoutInput.text.isEmpty()) {
-                return TextRange.Zero
-            }
-            val maxOffset = textLayoutResult.layoutInput.text.length
-            val startBoundary = boundaryFun(newRawSelection.start.coerceIn(0, maxOffset))
-            val endBoundary = boundaryFun(newRawSelection.end.coerceIn(0, maxOffset))
-
-            // If handles are not crossed, start should be snapped to the start of the word
-            // containing the start offset, and end should be snapped to the end of the word
-            // containing the end offset. If handles are crossed, start should be snapped to the
-            // end of the word containing the start offset, and end should be snapped to the start
-            // of the word containing the end offset.
-            val start = if (newRawSelection.reversed) startBoundary.end else startBoundary.start
-            val end = if (newRawSelection.reversed) endBoundary.start else endBoundary.end
-            return TextRange(start, end)
-        }
-
         /**
          * A special version of character based selection that accelerates the selection update
          * with word based selection. In short, it expands by word and shrinks by character.
          * Here is more details of the behavior:
          * 1. When previous selection is null, it will use word based selection.
-         * 2. When the start/end offset has moved to a different line, it will use word
+         * 2. When the start/end offset has moved to a different line/Text, it will use word
          * based selection.
          * 3. When the selection is shrinking, it behave same as the character based selection.
          * Shrinking means that the start/end offset is moving in the direction that makes
@@ -187,280 +99,319 @@
          *  Notice that this selection adjustment assumes that when isStartHandle is true, only
          *  start handle is moving(or unchanged), and vice versa.
          */
-        val CharacterWithWordAccelerate = object : SelectionAdjustment {
-            override fun adjust(
-                textLayoutResult: TextLayoutResult,
-                newRawSelectionRange: TextRange,
-                previousHandleOffset: Int,
-                isStartHandle: Boolean,
-                previousSelectionRange: TextRange?
-            ): TextRange {
-                // Previous selection is null. We start a word based selection.
-                if (previousSelectionRange == null) {
-                    return Word.adjust(
-                        textLayoutResult = textLayoutResult,
-                        newRawSelectionRange = newRawSelectionRange,
-                        previousHandleOffset = previousHandleOffset,
-                        isStartHandle = isStartHandle,
-                        previousSelectionRange = null
-                    )
-                }
+        val CharacterWithWordAccelerate = SelectionAdjustment { layout ->
+            val previousSelection = layout.previousSelection
+                ?: return@SelectionAdjustment Word.adjust(layout)
 
-                // if previous is collapsed, allow the current to continue to be collapsed.
-                // Otherwise, starting a selection may have a collapsed selection,
-                // but moving even a pixel will result in a different selection
-                // because the following code will ensure at least one character is selected.
-                if (
-                    previousSelectionRange.collapsed &&
-                    newRawSelectionRange == previousSelectionRange
-                ) {
-                    return previousSelectionRange
-                }
+            val previousAnchor: Selection.AnchorInfo
+            val newAnchor: Selection.AnchorInfo
+            val startAnchor: Selection.AnchorInfo
+            val endAnchor: Selection.AnchorInfo
 
-                val start: Int
-                val end: Int
-                if (isStartHandle) {
-                    start = updateSelectionBoundary(
-                        textLayoutResult = textLayoutResult,
-                        newRawOffset = newRawSelectionRange.start,
-                        previousRawOffset = previousHandleOffset,
-                        previousAdjustedOffset = previousSelectionRange.start,
-                        otherBoundaryOffset = newRawSelectionRange.end,
-                        isStart = true,
-                        isReversed = newRawSelectionRange.reversed
-                    )
-                    end = newRawSelectionRange.end
-                } else {
-                    start = newRawSelectionRange.start
-                    end = updateSelectionBoundary(
-                        textLayoutResult = textLayoutResult,
-                        newRawOffset = newRawSelectionRange.end,
-                        previousRawOffset = previousHandleOffset,
-                        previousAdjustedOffset = previousSelectionRange.end,
-                        otherBoundaryOffset = newRawSelectionRange.start,
-                        isStart = false,
-                        isReversed = newRawSelectionRange.reversed
-                    )
-                }
-
-                return TextRange(start, end)
+            if (layout.isStartHandle) {
+                previousAnchor = previousSelection.start
+                newAnchor = layout.updateSelectionBoundary(layout.startInfo, previousAnchor)
+                startAnchor = newAnchor
+                endAnchor = previousSelection.end
+            } else {
+                previousAnchor = previousSelection.end
+                newAnchor = layout.updateSelectionBoundary(layout.endInfo, previousAnchor)
+                startAnchor = previousSelection.start
+                endAnchor = newAnchor
             }
 
-            /**
-             * Helper function that updates start or end boundary of the selection. It implements
-             * the "expand by word and shrink by character behavior".
-             *
-             * @param textLayoutResult the text layout result
-             * @param newRawOffset the new raw offset of the selection boundary after the movement.
-             * @param previousRawOffset the raw offset of the updated selection boundary before the
-             * movement. In the case where previousRawOffset invalid(when selection update is
-             * triggered by long-press or click) pass -1 for this parameter.
-             * @param previousAdjustedOffset the previous final/adjusted offset. It's the current
-             * @param otherBoundaryOffset the offset of the other selection boundary. It is used
-             * to avoid empty selection in word based selection mode.
-             * selection boundary.
-             * @param isStart whether it's updating the selection start or end boundary.
-             * @param isReversed whether the selection is reversed or not. We use
-             * this information to determine if the selection is expanding or shrinking.
-             */
-            private fun updateSelectionBoundary(
-                textLayoutResult: TextLayoutResult,
-                newRawOffset: Int,
-                previousRawOffset: Int,
-                previousAdjustedOffset: Int,
-                otherBoundaryOffset: Int,
-                isStart: Boolean,
-                isReversed: Boolean
-            ): Int {
-                // The raw offset didn't change, directly return the previous adjusted start offset.
-                if (newRawOffset == previousRawOffset) {
-                    return previousAdjustedOffset
-                }
-
-                val currentLine = textLayoutResult.getLineForOffset(newRawOffset)
-                val previousLine = textLayoutResult.getLineForOffset(previousAdjustedOffset)
-
-                // The updating selection boundary has crossed a line, use word based selection.
-                if (currentLine != previousLine) {
-                    return snapToWordBoundary(
-                        textLayoutResult = textLayoutResult,
-                        newRawOffset = newRawOffset,
-                        currentLine = currentLine,
-                        otherBoundaryOffset = otherBoundaryOffset,
-                        isStart = isStart,
-                        isReversed = isReversed
-                    )
-                }
-
-                // Check if the start or end selection boundary is expanding. If it's shrinking,
-                // use character based selection.
-                if (!isExpanding(newRawOffset, previousRawOffset, isStart, isReversed)) {
-                    return newRawOffset
-                }
-
-                // If the previous start/end offset is not at a word boundary, which is indicating
-                // that start/end offset is updating within a word. In this case, it still uses
-                // character based selection.
-                if (!textLayoutResult.isAtWordBoundary(previousAdjustedOffset)) {
-                    return newRawOffset
-                }
-
-                // At this point we know, the updating start/end offset is still in the same line,
-                // it's expanding the selection, and it's not updating within a word. It should
-                // use word based selection.
-                return snapToWordBoundary(
-                    textLayoutResult = textLayoutResult,
-                    newRawOffset = newRawOffset,
-                    currentLine = currentLine,
-                    otherBoundaryOffset = otherBoundaryOffset,
-                    isStart = isStart,
-                    isReversed = isReversed
-                )
-            }
-
-            private fun snapToWordBoundary(
-                textLayoutResult: TextLayoutResult,
-                newRawOffset: Int,
-                currentLine: Int,
-                otherBoundaryOffset: Int,
-                isStart: Boolean,
-                isReversed: Boolean
-            ): Int {
-                val wordBoundary = textLayoutResult.getWordBoundary(newRawOffset)
-
-                // In the case where the target word crosses multiple lines due to hyphenation or
-                // being too long, we use the line start/end to keep the adjusted offset at the
-                // same line.
-                val wordStartLine = textLayoutResult.getLineForOffset(wordBoundary.start)
-                val start = if (wordStartLine == currentLine) {
-                    wordBoundary.start
-                } else {
-                    textLayoutResult.getLineStart(currentLine)
-                }
-
-                val wordEndLine = textLayoutResult.getLineForOffset(wordBoundary.end)
-                val end = if (wordEndLine == currentLine) {
-                    wordBoundary.end
-                } else {
-                    textLayoutResult.getLineEnd(currentLine)
-                }
-
-                // If one of the word boundary is exactly same as the otherBoundaryOffset, we
-                // can't snap to this word boundary since it will result in an empty selection
-                // range.
-                if (start == otherBoundaryOffset) {
-                    return end
-                }
-                if (end == otherBoundaryOffset) {
-                    return start
-                }
-
-                return if (isStart xor isReversed) {
-                    // In this branch when:
-                    // 1. selection is updating the start offset, and selection is not reversed.
-                    // 2. selection is updating the end offset, and selection is reversed.
-                    if (newRawOffset <= end) start else end
-                } else {
-                    // In this branch when:
-                    // 1. selection is updating the end offset, and selection is not reversed.
-                    // 2. selection is updating the start offset, and selection is reversed.
-                    if (newRawOffset >= start) end else start
-                }
-            }
-
-            private fun TextLayoutResult.isAtWordBoundary(offset: Int): Boolean {
-                val wordBoundary = getWordBoundary(offset)
-                return offset == wordBoundary.start || offset == wordBoundary.end
-            }
-
-            private fun isExpanding(
-                newRawOffset: Int,
-                previousRawOffset: Int,
-                isStart: Boolean,
-                previousReversed: Boolean
-            ): Boolean {
-                // -1 is considered as no previous offset, so the selection is expanding.
-                if (previousRawOffset == -1) {
-                    return true
-                }
-                if (newRawOffset == previousRawOffset) {
-                    return false
-                }
-                return if (isStart xor previousReversed) {
-                    newRawOffset < previousRawOffset
-                } else {
-                    newRawOffset > previousRawOffset
-                }
+            if (newAnchor == previousAnchor) {
+                // This avoids some cases in BiDi where `layout.crossed` is incorrect.
+                // In BiDi layout, a single character move gesture can result in the offset
+                // changing a large amount when crossing over from LTR -> RTL or visa versa.
+                // This can result in a layout which says it is crossed, but our new selection
+                // is uncrossed. Instead, just re-use the old selection.
+                // It also saves an allocation.
+                previousSelection
+            } else {
+                val crossed = layout.crossStatus == CrossStatus.CROSSED ||
+                    (layout.crossStatus == CrossStatus.COLLAPSED &&
+                        startAnchor.offset > endAnchor.offset)
+                Selection(startAnchor, endAnchor, crossed).ensureAtLeastOneChar(layout)
             }
         }
     }
 }
 
 /**
- * This method adjusts the raw start and end offset and bounds the selection to one character
- * respecting [String.findPrecedingBreak] and [String.findFollowingBreak]. The logic of bounding
- * evaluates the last selection result, which handle is being dragged, and if selection reaches the
- * boundary.
- *
- * @param text the complete string
- * @param offset unprocessed start and end offset calculated directly from input position, in
- * this case start and offset equals to each other.
- * @param lastOffset last offset of the text. It's actually the length of the text.
- * @param isStartHandle true if the start handle is being dragged
- * @param previousHandlesCrossed true if the selection handles are crossed in the previous
- * selection. This function will try to maintain the handle cross state. This can help make
- * selection stable.
- *
- * @return the adjusted [TextRange].
+ * @receiver The selection layout. It is expected that its previousSelection is non-null
  */
-internal fun ensureAtLeastOneChar(
-    text: String,
-    offset: Int,
-    lastOffset: Int,
-    isStartHandle: Boolean,
-    previousHandlesCrossed: Boolean
-): TextRange {
-    // When lastOffset is 0, it can only return an empty TextRange.
-    // When previousSelection is null, it won't start a selection and return an empty TextRange.
-    if (lastOffset == 0) return TextRange(offset, offset)
+private fun SelectionLayout.updateSelectionBoundary(
+    info: SelectableInfo,
+    previousSelectionAnchor: Selection.AnchorInfo
+): Selection.AnchorInfo {
+    val currentRawOffset =
+        if (isStartHandle) info.rawStartHandleOffset
+        else info.rawEndHandleOffset
 
-    // When offset is at the boundary, the handle that is not dragged should be at [offset]. Here
-    // the other handle's position is computed accordingly.
-    if (offset == 0) {
-        val followingBreak = text.findFollowingBreak(0)
-        return if (isStartHandle) {
-            TextRange(followingBreak, 0)
-        } else {
-            TextRange(0, followingBreak)
-        }
+    val currentSlot = if (isStartHandle) startSlot else endSlot
+    if (currentSlot != info.slot) {
+        // we are between Texts
+        return info.anchorForOffset(currentRawOffset)
     }
 
-    if (offset == lastOffset) {
-        val precedingBreak = text.findPrecedingBreak(lastOffset)
-        return if (isStartHandle) {
-            TextRange(precedingBreak, lastOffset)
-        } else {
-            TextRange(lastOffset, precedingBreak)
-        }
+    val currentRawLine by lazy(LazyThreadSafetyMode.NONE) {
+        info.textLayoutResult.getLineForOffset(currentRawOffset)
     }
 
-    // In other cases, this function will try to maintain the current cross handle states.
-    // Only in this way the selection can be stable.
-    return if (isStartHandle) {
-        if (!previousHandlesCrossed) {
-            // Handle is NOT crossed, and the start handle is dragged.
-            TextRange(text.findPrecedingBreak(offset), offset)
-        } else {
-            // Handle is crossed, and the start handle is dragged.
-            TextRange(text.findFollowingBreak(offset), offset)
-        }
+    val otherRawOffset =
+        if (isStartHandle) info.rawEndHandleOffset
+        else info.rawStartHandleOffset
+
+    val anchorSnappedToWordBoundary by lazy(LazyThreadSafetyMode.NONE) {
+        info.snapToWordBoundary(
+            currentLine = currentRawLine,
+            currentOffset = currentRawOffset,
+            otherOffset = otherRawOffset,
+            isStart = isStartHandle,
+            crossed = crossStatus == CrossStatus.CROSSED
+        )
+    }
+
+    if (info.selectableId != previousSelectionAnchor.selectableId) {
+        // moved to an entirely new Text, use word based adjustment
+        return anchorSnappedToWordBoundary
+    }
+
+    if (currentRawOffset == info.rawPreviousHandleOffset) {
+        // no change in current handle, return the previous result unchanged
+        return previousSelectionAnchor
+    }
+
+    val previousSelectionOffset = previousSelectionAnchor.offset
+    val previousSelectionLine =
+        info.textLayoutResult.getLineForOffset(previousSelectionOffset)
+
+    if (currentRawLine != previousSelectionLine) {
+        // line changed, use word based adjustment
+        return anchorSnappedToWordBoundary
+    }
+
+    val previousSelectionWordBoundary =
+        info.textLayoutResult.getWordBoundary(previousSelectionOffset)
+
+    if (!info.isExpanding(currentRawOffset, isStartHandle)) {
+        // we're shrinking, use the raw offset.
+        return info.anchorForOffset(currentRawOffset)
+    }
+
+    if (previousSelectionOffset == previousSelectionWordBoundary.start ||
+        previousSelectionOffset == previousSelectionWordBoundary.end
+    ) {
+        // We are expanding, and the previous offset was a word boundary,
+        // so continue using word boundaries.
+        return anchorSnappedToWordBoundary
+    }
+
+    // We're expanding, but our previousOffset was not at a word boundary. This means
+    // we are adjusting a selection within a word already, so continue to do so.
+    return info.anchorForOffset(currentRawOffset)
+}
+
+private fun SelectableInfo.isExpanding(
+    currentRawOffset: Int,
+    isStart: Boolean
+): Boolean {
+    if (rawPreviousHandleOffset == -1) {
+        return true
+    }
+    if (currentRawOffset == rawPreviousHandleOffset) {
+        return false
+    }
+
+    val crossed = rawCrossStatus == CrossStatus.CROSSED
+    return if (isStart xor crossed) {
+        currentRawOffset < rawPreviousHandleOffset
     } else {
-        if (!previousHandlesCrossed) {
-            // Handle is NOT crossed, and the end handle is dragged.
-            TextRange(offset, text.findFollowingBreak(offset))
-        } else {
-            // Handle is crossed, and the end handle is dragged.
-            TextRange(offset, text.findPrecedingBreak(offset))
+        currentRawOffset > rawPreviousHandleOffset
+    }
+}
+
+private fun SelectableInfo.snapToWordBoundary(
+    currentLine: Int,
+    currentOffset: Int,
+    otherOffset: Int,
+    isStart: Boolean,
+    crossed: Boolean,
+): Selection.AnchorInfo {
+    val wordBoundary = textLayoutResult.getWordBoundary(currentOffset)
+
+    // In the case where the target word crosses multiple lines due to hyphenation or
+    // being too long, we use the line start/end to keep the adjusted offset at the
+    // same line.
+    val wordStartLine = textLayoutResult.getLineForOffset(wordBoundary.start)
+    val start = if (wordStartLine == currentLine) {
+        wordBoundary.start
+    } else {
+        textLayoutResult.getLineStart(currentLine)
+    }
+
+    val wordEndLine = textLayoutResult.getLineForOffset(wordBoundary.end)
+    val end = if (wordEndLine == currentLine) {
+        wordBoundary.end
+    } else {
+        textLayoutResult.getLineEnd(currentLine)
+    }
+
+    // If one of the word boundary is exactly same as the otherBoundaryOffset, we
+    // can't snap to this word boundary since it will result in an empty selection
+    // range.
+    if (start == otherOffset) {
+        return anchorForOffset(end)
+    }
+    if (end == otherOffset) {
+        return anchorForOffset(start)
+    }
+
+    val resultOffset = if (isStart xor crossed) {
+        // In this branch when:
+        // 1. selection is updating the start offset, and selection is not reversed.
+        // 2. selection is updating the end offset, and selection is reversed.
+        if (currentOffset <= end) start else end
+    } else {
+        // In this branch when:
+        // 1. selection is updating the end offset, and selection is not reversed.
+        // 2. selection is updating the start offset, and selection is reversed.
+        if (currentOffset >= start) end else start
+    }
+
+    return anchorForOffset(resultOffset)
+}
+
+private fun interface BoundaryFunction {
+    fun SelectableInfo.getBoundary(offset: Int): TextRange
+}
+
+private fun adjustToBoundaries(
+    layout: SelectionLayout,
+    boundaryFunction: BoundaryFunction,
+): Selection {
+    val crossed = layout.crossStatus == CrossStatus.CROSSED
+    return Selection(
+        start = layout.startInfo.anchorOnBoundary(
+            crossed = crossed,
+            isStart = true,
+            slot = layout.startSlot,
+            boundaryFunction = boundaryFunction,
+        ),
+        end = layout.endInfo.anchorOnBoundary(
+            crossed = crossed,
+            isStart = false,
+            slot = layout.endSlot,
+            boundaryFunction = boundaryFunction,
+        ),
+        handlesCrossed = crossed
+    )
+}
+
+private fun SelectableInfo.anchorOnBoundary(
+    crossed: Boolean,
+    isStart: Boolean,
+    slot: Int,
+    boundaryFunction: BoundaryFunction,
+): Selection.AnchorInfo {
+    val offset = if (isStart) rawStartHandleOffset else rawEndHandleOffset
+
+    if (slot != this.slot) {
+        return anchorForOffset(offset)
+    }
+
+    val range = with(boundaryFunction) {
+        getBoundary(offset)
+    }
+
+    return anchorForOffset(if (isStart xor crossed) range.start else range.end)
+}
+
+/**
+ * This method adjusts the selection to one character respecting [String.findPrecedingBreak]
+ * and [String.findFollowingBreak].
+ */
+internal fun Selection.ensureAtLeastOneChar(layout: SelectionLayout): Selection {
+    // There already is at least one char in this selection, return this selection unchanged.
+    if (!isCollapsed(layout)) {
+        return this
+    }
+
+    // Exceptions where 0 char selection is acceptable:
+    //   - The selection crosses multiple Texts, but is still collapsed.
+    //       - In the same situation in a single Text, we usually select some whitespace.
+    //         Since there is no whitespace to select, select nothing. Expanding the selection
+    //         into any Texts in this case is likely confusing to the user
+    //         as it is different functionality compared to single text.
+    //   - The previous selection is null, indicating this is the start of a selection.
+    //       - This allows a selection to start off as collapsed. This is necessary for
+    //         Character adjustment to allow an initial collapsed selection, and then once a
+    //         non-collapsed selection is started, this exception goes away.
+    //   - There is no text to select at all, so you can't expand anywhere.
+    val text = layout.currentInfo.inputText
+    if (layout.size > 1 || layout.previousSelection == null || text.isEmpty()) {
+        return this
+    }
+
+    return expandOneChar(layout)
+}
+
+/**
+ * Precondition: the selection is empty.
+ */
+private fun Selection.expandOneChar(layout: SelectionLayout): Selection {
+    val info = layout.currentInfo
+    val text = info.inputText
+    val offset = info.rawStartHandleOffset // start and end are the same, so either works
+
+    // when the offset is at either boundary of the text,
+    // expand the current handle one character into the text from the boundary.
+    val lastOffset = text.length
+    return when (offset) {
+        0 -> {
+            val followingBreak = text.findFollowingBreak(0)
+            if (layout.isStartHandle) {
+                copy(start = start.changeOffset(info, followingBreak), handlesCrossed = true)
+            } else {
+                copy(end = end.changeOffset(info, followingBreak), handlesCrossed = false)
+            }
+        }
+
+        lastOffset -> {
+            val precedingBreak = text.findPrecedingBreak(lastOffset)
+            if (layout.isStartHandle) {
+                copy(start = start.changeOffset(info, precedingBreak), handlesCrossed = false)
+            } else {
+                copy(end = end.changeOffset(info, precedingBreak), handlesCrossed = true)
+            }
+        }
+
+        else -> {
+            // In cases where offset is not along the boundary,
+            // we will try to maintain the current cross handle states.
+            val crossed = layout.previousSelection?.handlesCrossed == true
+            val newOffset =
+                if (layout.isStartHandle xor crossed) {
+                    text.findPrecedingBreak(offset)
+                } else {
+                    text.findFollowingBreak(offset)
+                }
+
+            if (layout.isStartHandle) {
+                copy(start = start.changeOffset(info, newOffset), handlesCrossed = crossed)
+            } else {
+                copy(end = end.changeOffset(info, newOffset), handlesCrossed = crossed)
+            }
         }
     }
 }
+
+// update direction when we are changing the offset since it may be different
+private fun Selection.AnchorInfo.changeOffset(
+    info: SelectableInfo,
+    newOffset: Int,
+): Selection.AnchorInfo = copy(
+    offset = newOffset,
+    direction = info.textLayoutResult.getBidiRunDirection(newOffset)
+)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index 059d464..5a570fc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.pointerInput
@@ -81,7 +82,10 @@
     onSelectionChange: (Selection?) -> Unit,
     children: @Composable () -> Unit
 ) {
-    val registrarImpl = remember { SelectionRegistrarImpl() }
+    val registrarImpl = rememberSaveable(saver = SelectionRegistrarImpl.Saver) {
+        SelectionRegistrarImpl()
+    }
+
     val manager = remember { SelectionManager(registrarImpl) }
 
     manager.hapticFeedBack = LocalHapticFeedback.current
@@ -96,7 +100,10 @@
             // cross-composable selection.
             SimpleLayout(modifier = modifier.then(manager.modifier)) {
                 children()
-                if (manager.isInTouchMode && manager.hasFocus && manager.isNonEmptySelection()) {
+                if (manager.isInTouchMode &&
+                    manager.hasFocus &&
+                    !manager.isTriviallyCollapsedSelection()
+                ) {
                     manager.selection?.let {
                         listOf(true, false).fastForEach { isStartHandle ->
                             val observer = remember(isStartHandle) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionGestures.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionGestures.kt
index 07857d76..4909bad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionGestures.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionGestures.kt
@@ -165,18 +165,16 @@
             observer.onDragDone()
         }
     } else {
-        val selectionMode = when (clicksCounter.clicks) {
-            // TODO(b/281585400) switch 1 to Character adjustment.
-            //     This will result in multi text bugs,
-            //     like a blank line selection resulting in a single char being selected.
+        val selectionAdjustment = when (clicksCounter.clicks) {
             1 -> SelectionAdjustment.None
             2 -> SelectionAdjustment.Word
             else -> SelectionAdjustment.Paragraph
         }
-        val started = observer.onStart(downChange.position, selectionMode)
+
+        val started = observer.onStart(downChange.position, selectionAdjustment)
         if (started) {
             val shouldConsumeUp = drag(downChange.id) {
-                if (observer.onDrag(it.position, selectionMode)) {
+                if (observer.onDrag(it.position, selectionAdjustment)) {
                     it.consume()
                 }
             }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt
new file mode 100644
index 0000000..f7c4e2c
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt
@@ -0,0 +1,657 @@
+/*
+ * 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.selection
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.util.fastForEachIndexed
+
+/**
+ * Selection data around how the pointer relates to the actual positions of the Text components.
+ *
+ * # Explanation of Slots
+ *
+ * The Slot value is meant to sit either *on* an index or *between* indices.
+ * The former means the pointer is on a `Text` (like slot value `1` and index `0` below).
+ * The latter means the pointer is not on a `Text`, but between `Text`s (like slot value
+ * `0` or `2` below). So a slot value of `2` means that the pointer is between
+ * the `Text`s at index `0` and `1`, perhaps in padding or a non-selectable `Text`.
+ *
+ * ```
+ * slot value  0  1  2  3  4  5  6  7  8  9  10
+ * info index     0     1     2     3     4
+ * ```
+ *
+ * ## Mappings:
+ * The `X` represents an impossible slot assignment
+ * The `|`, `/`, and `\` represent a slot mapping.
+ *
+ * ### Mapping minimum slot:
+ * ```
+ * slot value  0  1  2  3  4  5  6  7  8  9  10
+ *              \ |   \ |   \ |   \ |   \ | X
+ * info index     0     1     2     3     4
+ *
+ * min-slot index = slot / 2
+ * ```
+ *
+ * Minimum slot cannot be after the final `Text` (index `4`).
+ *
+ * ### Mapping maximum slot:
+ * ```
+ * slot value  0  1  2  3  4  5  6  7  8  9  10
+ *              X | /   | /   | /   | /   | /
+ * info index     0     1     2     3     4
+ * max-slot index = (slot - 1) / 2
+ * ```
+ *
+ * Maximum slot cannot be before the first `Text` (index `0`).
+ *
+ * ## Assertions
+ * * The non-dragging slot should always be directly on a text (odd) because the non-dragging
+ * handle must be anchored somewhere.
+ *     * Because of this, we can determine that if `startSlot == endSlot` then it also follows that
+ *       `startSlot` and `endSlot` are even.
+ */
+internal interface SelectionLayout {
+    /** The number of [SelectableInfo]s in this [SelectionLayout]. */
+    val size: Int
+
+    /** The slot of the start anchor. */
+    val startSlot: Int
+
+    /** The slot of the end anchor. */
+    val endSlot: Int
+
+    /** The [CrossStatus] of this layout as determined by the slot/offsets. */
+    val crossStatus: CrossStatus
+
+    /** The [SelectableInfo] that the start is on. */
+    val startInfo: SelectableInfo
+
+    /** The [SelectableInfo] that the end is on. */
+    val endInfo: SelectableInfo
+
+    /** The [SelectableInfo] that the start/end is on as determined by [isStartHandle]. */
+    val currentInfo: SelectableInfo
+
+    /** The [SelectableInfo] for the first selectable on the screen. */
+    val firstInfo: SelectableInfo
+
+    /** The [SelectableInfo] for the last selectable on the screen. */
+    val lastInfo: SelectableInfo
+
+    /**
+     * Run a function on every [SelectableInfo] between [firstInfo] and [lastInfo]
+     * (not including [firstInfo]/[lastInfo]).
+     */
+    fun forEachMiddleInfo(block: (SelectableInfo) -> Unit)
+
+    /** Whether the start or end anchor is currently being moved. */
+    val isStartHandle: Boolean
+
+    /** The previous [Selection] that we are modifying. */
+    val previousSelection: Selection?
+
+    /**
+     * Whether this layout, compared to another layout, has any relevant changes that would require
+     * recomputing selection.
+     * @param other the selection layout to check for changes compared to this one
+     */
+    fun shouldRecomputeSelection(other: SelectionLayout?): Boolean
+
+    /**
+     * Splits a selection into a Map of selectable ID to Selections limited to that selectable ID.
+     *
+     * @param selection The selection to turn into subSelections
+     */
+    fun createSubSelections(selection: Selection): Map<Long, Selection>
+}
+
+private class MultiSelectionLayout(
+    val selectableIdToInfoListIndex: Map<Long, Int>,
+    val infoList: List<SelectableInfo>,
+    override val startSlot: Int,
+    override val endSlot: Int,
+    override val isStartHandle: Boolean,
+    override val previousSelection: Selection?,
+) : SelectionLayout {
+    init {
+        check(infoList.size > 1) {
+            "MultiSelectionLayout requires an infoList size greater than 1, was ${infoList.size}."
+        }
+    }
+
+    // Most of these properties are unused unless shouldRecomputeSelection returns true,
+    // hence why getters are used everywhere.
+
+    override val size get() = infoList.size
+
+    override val crossStatus: CrossStatus
+        get() = when {
+            startSlot < endSlot -> CrossStatus.NOT_CROSSED
+            startSlot > endSlot -> CrossStatus.CROSSED
+            // because one of the slots is not-dragging, it must be on a text directly
+            // because one of the slots is on a text directly and the start/end slots are equal,
+            // they both must be odd. Given this, dividing the slot by 2 should give us the correct
+            // info index.
+            else -> infoList[startSlot / 2].rawCrossStatus
+        }
+
+    override val startInfo: SelectableInfo
+        get() = infoList[startOrEndSlotToIndex(startSlot, isStartSlot = true)]
+
+    override val endInfo: SelectableInfo
+        get() = infoList[startOrEndSlotToIndex(endSlot, isStartSlot = false)]
+
+    override val currentInfo: SelectableInfo
+        get() = if (isStartHandle) startInfo else endInfo
+
+    override val firstInfo: SelectableInfo
+        get() = if (crossStatus == CrossStatus.CROSSED) endInfo else startInfo
+
+    override val lastInfo: SelectableInfo
+        get() = if (crossStatus == CrossStatus.CROSSED) startInfo else endInfo
+
+    override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
+        val minIndex = getInfoListIndexBySelectableId(firstInfo.selectableId)
+        val maxIndex = getInfoListIndexBySelectableId(lastInfo.selectableId)
+        if (minIndex + 1 >= maxIndex) {
+            return
+        }
+
+        for (i in minIndex + 1 until maxIndex) {
+            block(infoList[i])
+        }
+    }
+
+    override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
+        previousSelection == null ||
+            other == null ||
+            other !is MultiSelectionLayout ||
+            isStartHandle != other.isStartHandle ||
+            startSlot != other.startSlot ||
+            endSlot != other.endSlot ||
+            shouldAnyInfoRecomputeSelection(other)
+
+    private fun shouldAnyInfoRecomputeSelection(other: MultiSelectionLayout): Boolean {
+        if (size != other.size) return true
+        for (i in infoList.indices) {
+            val thisInfo = infoList[i]
+            val otherInfo = other.infoList[i]
+            if (thisInfo.shouldRecomputeSelection(otherInfo)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    override fun createSubSelections(selection: Selection): Map<Long, Selection> =
+        // Selection is within one selectable, we can return a singleton map of this selection.
+        if (selection.start.selectableId == selection.end.selectableId) {
+            // this check, if not passed, leads to exceptions when selection
+            // highlighting is rendered, so check here instead.
+            check(
+                (selection.handlesCrossed && selection.start.offset >= selection.end.offset) ||
+                    (!selection.handlesCrossed && selection.start.offset <= selection.end.offset)
+            ) {
+                "unexpectedly miss-crossed selection: $selection"
+            }
+            mapOf(selection.start.selectableId to selection)
+        } else buildMap {
+            val minAnchor = with(selection) { if (handlesCrossed) end else start }
+            createAndPutSubSelection(selection, firstInfo, minAnchor.offset, firstInfo.textLength)
+
+            forEachMiddleInfo { info ->
+                createAndPutSubSelection(selection, info, minOffset = 0, info.textLength)
+            }
+
+            val maxAnchor = with(selection) { if (handlesCrossed) start else end }
+            createAndPutSubSelection(selection, lastInfo, minOffset = 0, maxAnchor.offset)
+        }
+
+    private fun MutableMap<Long, Selection>.createAndPutSubSelection(
+        selection: Selection,
+        info: SelectableInfo,
+        minOffset: Int,
+        maxOffset: Int
+    ) {
+        val subSelection = if (selection.handlesCrossed) {
+            info.makeSingleLayoutSelection(start = maxOffset, end = minOffset)
+        } else {
+            info.makeSingleLayoutSelection(start = minOffset, end = maxOffset)
+        }
+
+        // this check, if not passed, leads to exceptions when selection
+        // highlighting is rendered, so check here instead.
+        check(minOffset <= maxOffset) {
+            "minOffset should be less than or equal to maxOffset: $subSelection"
+        }
+
+        put(info.selectableId, subSelection)
+    }
+
+    override fun toString(): String = "MultiSelectionLayout(isStartHandle=$isStartHandle, " +
+        "startPosition=${(startSlot + 1).toFloat() / 2}, " +
+        "endPosition=${(endSlot + 1).toFloat() / 2}, " +
+        "crossed=$crossStatus, " +
+        "infos=${
+            buildString {
+                append("[\n\t")
+                var first = true
+                infoList
+                    .fastForEachIndexed { index, info ->
+                        if (first) {
+                            first = false
+                        } else {
+                            append(",\n\t")
+                        }
+                        append("${(index + 1)} -> $info")
+                    }
+                append("\n]")
+            }
+        })"
+
+    private fun startOrEndSlotToIndex(slot: Int, isStartSlot: Boolean): Int =
+        slotToIndex(
+            slot = slot,
+            isMinimumSlot = when (crossStatus) {
+                // collapsed: doesn't matter whether true or false, it will result in the same index
+                CrossStatus.COLLAPSED -> true
+                CrossStatus.NOT_CROSSED -> isStartSlot
+                CrossStatus.CROSSED -> !isStartSlot
+            }
+        )
+
+    private fun slotToIndex(slot: Int, isMinimumSlot: Boolean): Int {
+        val slotAdjustment = if (isMinimumSlot) 0 else 1
+        return (slot - slotAdjustment) / 2
+    }
+
+    private fun getInfoListIndexBySelectableId(id: Long): Int =
+        requireNotNull(selectableIdToInfoListIndex[id]) {
+            "Invalid selectableId: $id"
+        }
+}
+
+/**
+ * Create a selection layout that has only one slot.
+ *
+ * @param isStartHandle whether this is the start or end anchor
+ * @param previousSelection the previous selection
+ * @param info the single [SelectableInfo]
+ */
+private class SingleSelectionLayout(
+    override val isStartHandle: Boolean,
+    override val previousSelection: Selection?,
+    private val info: SelectableInfo,
+) : SelectionLayout {
+    companion object {
+        const val DEFAULT_SLOT = 1
+        const val DEFAULT_SELECTABLE_ID = 1L
+    }
+
+    override val size get() = 1
+    override val startSlot get() = info.slot
+    override val endSlot get() = info.slot
+    override val crossStatus: CrossStatus get() = info.rawCrossStatus
+    override val startInfo: SelectableInfo get() = info
+    override val endInfo: SelectableInfo get() = info
+    override val currentInfo: SelectableInfo get() = info
+    override val firstInfo: SelectableInfo get() = info
+    override val lastInfo: SelectableInfo get() = info
+
+    override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
+        // there are no middle infos, so do nothing
+    }
+
+    override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
+        previousSelection == null ||
+            other == null ||
+            other !is SingleSelectionLayout ||
+            isStartHandle != other.isStartHandle ||
+            info.shouldRecomputeSelection(other.info)
+
+    override fun createSubSelections(selection: Selection): Map<Long, Selection> {
+        check(
+            (selection.handlesCrossed && selection.start.offset >= selection.end.offset) ||
+                (!selection.handlesCrossed && selection.start.offset <= selection.end.offset)
+        ) {
+            "unexpectedly miss-crossed selection: $selection"
+        }
+        return mapOf(info.selectableId to selection)
+    }
+
+    override fun toString(): String =
+        "SingleSelectionLayout(isStartHandle=$isStartHandle, crossed=$crossStatus, info=\n\t$info)"
+}
+
+/**
+ * Create a selection layout that has only one slot.
+ *
+ * This is intended for TextField, where multiple selectables is of no concern.
+ *
+ * @param layoutResult the [TextLayoutResult] for the text field
+ * @param rawStartHandleOffset the index of the start handle
+ * @param rawEndHandleOffset the index of the end handle
+ * @param rawPreviousHandleOffset the previous handle offset based on [isStartHandle],
+ * or -1 if none
+ * @param previousSelectionRange the previous selection
+ * @param isStartOfSelection whether this is the start of a selection gesture (no previous context)
+ * @param isStartHandle whether this is the start or end anchor
+ */
+internal fun getTextFieldSelectionLayout(
+    layoutResult: TextLayoutResult,
+    rawStartHandleOffset: Int,
+    rawEndHandleOffset: Int,
+    rawPreviousHandleOffset: Int,
+    previousSelectionRange: TextRange,
+    isStartOfSelection: Boolean,
+    isStartHandle: Boolean,
+): SelectionLayout = SingleSelectionLayout(
+    isStartHandle = isStartHandle,
+    previousSelection = if (isStartOfSelection) null else Selection(
+        start = Selection.AnchorInfo(
+            layoutResult.getTextDirectionForOffset(previousSelectionRange.start),
+            previousSelectionRange.start,
+            SingleSelectionLayout.DEFAULT_SELECTABLE_ID
+        ),
+        end = Selection.AnchorInfo(
+            layoutResult.getTextDirectionForOffset(previousSelectionRange.end),
+            previousSelectionRange.end,
+            SingleSelectionLayout.DEFAULT_SELECTABLE_ID
+        ),
+        handlesCrossed = previousSelectionRange.reversed
+    ),
+    info = SelectableInfo(
+        selectableId = SingleSelectionLayout.DEFAULT_SELECTABLE_ID,
+        slot = SingleSelectionLayout.DEFAULT_SLOT,
+        rawStartHandleOffset = rawStartHandleOffset,
+        rawEndHandleOffset = rawEndHandleOffset,
+        textLayoutResult = layoutResult,
+        rawPreviousHandleOffset = rawPreviousHandleOffset
+    ),
+)
+
+/**
+ * Whether something is crossed as determined by the position of the start/end.
+ */
+internal enum class CrossStatus {
+    /** The start comes after the end. */
+    CROSSED,
+
+    /** The start comes before the end. */
+    NOT_CROSSED,
+
+    /** The start is the same as the end. */
+    COLLAPSED
+}
+
+/**
+ * A builder for [SelectionLayout] that ensures the data structures and slots
+ * are properly constructed.
+ *
+ * @param previousHandlePosition the previous handle position matching the handle directed to by
+ * [isStartHandle]
+ * @param containerCoordinates the coordinates of the [SelectionContainer] for converting
+ * [SelectionContainer] coordinates to their respective [Selectable] coordinates
+ * @param isStartHandle whether the currently pressed/clicked handle is the start
+ * @param selectableIdOrderingComparator determines the ordering of selectables by their IDs
+ */
+internal class SelectionLayoutBuilder(
+    val startHandlePosition: Offset,
+    val endHandlePosition: Offset,
+    val previousHandlePosition: Offset,
+    val containerCoordinates: LayoutCoordinates,
+    val isStartHandle: Boolean,
+    val previousSelection: Selection?,
+    val selectableIdOrderingComparator: Comparator<Long>
+) {
+    private val selectableIdToInfoListIndex: MutableMap<Long, Int> = mutableMapOf()
+    private val infoList: MutableList<SelectableInfo> = mutableListOf()
+    private var startSlot: Int = -1
+    private var endSlot: Int = -1
+    private var currentSlot: Int = -1
+
+    /**
+     * Finishes building the [SelectionLayout] and returns it.
+     *
+     * @return the [SelectionLayout] or null if no [SelectableInfo]s were added.
+     */
+    fun build(): SelectionLayout {
+        return when (infoList.size) {
+            0 -> throw IllegalStateException(
+                "SelectionLayout must have at least one SelectableInfo."
+            )
+
+            1 -> SingleSelectionLayout(
+                info = infoList.single(),
+                previousSelection = previousSelection,
+                isStartHandle = isStartHandle,
+            )
+
+            else -> {
+                val lastSlot = currentSlot + 1
+                MultiSelectionLayout(
+                    selectableIdToInfoListIndex = selectableIdToInfoListIndex,
+                    infoList = infoList,
+                    startSlot = if (startSlot == -1) lastSlot else startSlot,
+                    endSlot = if (endSlot == -1) lastSlot else endSlot,
+                    isStartHandle = isStartHandle,
+                    previousSelection = previousSelection,
+                )
+            }
+        }
+    }
+
+    /**
+     * Appends a selection info to this builder.
+     */
+    fun appendInfo(
+        selectableId: Long,
+        rawStartHandleOffset: Int,
+        startHandleDirection: Direction,
+        rawEndHandleOffset: Int,
+        endHandleDirection: Direction,
+        rawPreviousHandleOffset: Int,
+        textLayoutResult: TextLayoutResult,
+    ): SelectableInfo {
+        // We need currentSlot to equal the slot of the "last" info when getLayout is called,
+        // so increment this before adding the info and leave the correct slot in place at the end.
+        currentSlot += 2
+
+        val selectableInfo = SelectableInfo(
+            selectableId = selectableId,
+            slot = currentSlot,
+            rawStartHandleOffset = rawStartHandleOffset,
+            rawEndHandleOffset = rawEndHandleOffset,
+            rawPreviousHandleOffset = rawPreviousHandleOffset,
+            textLayoutResult = textLayoutResult,
+        )
+
+        if (startSlot == -1) {
+            startSlot = updateBasedOnDirection(startHandleDirection)
+        }
+
+        if (endSlot == -1) {
+            endSlot = updateBasedOnDirection(endHandleDirection)
+        }
+
+        selectableIdToInfoListIndex[selectableId] = infoList.size
+        infoList += selectableInfo
+        return selectableInfo
+    }
+
+    private fun updateBasedOnDirection(direction: Direction): Int = when (direction) {
+        Direction.BEFORE -> currentSlot - 1
+        Direction.ON -> currentSlot
+        else -> -1
+    }
+}
+
+/**
+ * Where the position of a cursor/press is compared to a selectable.
+ */
+@JvmInline
+internal value class Direction(val direction: Int) {
+    companion object {
+        /** The cursor/press is before the selectable */
+        val BEFORE = Direction(-1)
+
+        /** The cursor/press is on the selectable */
+        val ON = Direction(0)
+
+        /** The cursor/press is after the selectable */
+        val AFTER = Direction(1)
+    }
+}
+
+/**
+ * Data about a specific selectable within a [SelectionLayout].
+ */
+internal class SelectableInfo(
+    val selectableId: Long,
+    val slot: Int,
+    val rawStartHandleOffset: Int,
+    val rawEndHandleOffset: Int,
+    val rawPreviousHandleOffset: Int,
+    val textLayoutResult: TextLayoutResult,
+) {
+
+    /** The [String] in the selectable. */
+    val inputText: String
+        get() = textLayoutResult.layoutInput.text.text
+
+    /** The length of the [String] in the selectable. */
+    val textLength: Int
+        get() = inputText.length
+
+    /** Whether the raw offsets of this info are crossed. */
+    val rawCrossStatus: CrossStatus
+        get() = when {
+            rawStartHandleOffset < rawEndHandleOffset -> CrossStatus.NOT_CROSSED
+            rawStartHandleOffset > rawEndHandleOffset -> CrossStatus.CROSSED
+            else -> CrossStatus.COLLAPSED
+        }
+
+    private val startRunDirection
+        get() = textLayoutResult.getTextDirectionForOffset(rawStartHandleOffset)
+
+    private val endRunDirection
+        get() = textLayoutResult.getTextDirectionForOffset(rawEndHandleOffset)
+
+    /**
+     * Whether this info, compared to another info, has any relevant changes that would require
+     * recomputing selection.
+     * @param other the selectable info to check for changes compared to this one
+     */
+    fun shouldRecomputeSelection(other: SelectableInfo): Boolean =
+        selectableId != other.selectableId ||
+            rawStartHandleOffset != other.rawStartHandleOffset ||
+            rawEndHandleOffset != other.rawEndHandleOffset
+
+    /**
+     * Get a [Selection.AnchorInfo] for this [SelectableInfo] at the given [offset].
+     */
+    fun anchorForOffset(offset: Int): Selection.AnchorInfo = Selection.AnchorInfo(
+        direction = textLayoutResult.getTextDirectionForOffset(offset),
+        offset = offset,
+        selectableId = selectableId
+    )
+
+    /**
+     * Get a [Selection] within the selectable represented by this [SelectableInfo]
+     * for the given [start] and [end] offsets.
+     */
+    fun makeSingleLayoutSelection(start: Int, end: Int): Selection = Selection(
+        start = anchorForOffset(start),
+        end = anchorForOffset(end),
+        handlesCrossed = start > end
+    )
+
+    override fun toString(): String = "SelectionInfo(id=$selectableId, " +
+        "range=($rawStartHandleOffset-$startRunDirection,$rawEndHandleOffset-$endRunDirection), " +
+        "prevOffset=$rawPreviousHandleOffset)"
+}
+
+/**
+ * Get the text direction for a given offset.
+ *
+ * This simply calls [TextLayoutResult.getBidiRunDirection] with one exception, if the offset is an
+ * empty line, then we defer to [TextLayoutResult.multiParagraph] and
+ * [androidx.compose.ui.text.MultiParagraph.getParagraphDirection]. This is because an empty line
+ * always resolves to LTR, even if the paragraph is RTL.
+ */
+// TODO(b/295197585)
+//   Can this logic be moved to a new method in `androidx.compose.ui.text.Paragraph`?
+private fun TextLayoutResult.getTextDirectionForOffset(offset: Int): ResolvedTextDirection =
+    if (isOffsetAnEmptyLine(offset)) getParagraphDirection(offset)
+    else getBidiRunDirection(offset)
+
+private fun TextLayoutResult.isOffsetAnEmptyLine(offset: Int): Boolean =
+    layoutInput.text.isEmpty() || getLineForOffset(offset).let { currentLine ->
+        // verify the previous and next offsets either don't exist because they're at a boundary
+        // or that they are different lines than the current line.
+        (offset == 0 || currentLine != getLineForOffset(offset - 1)) &&
+            (offset == layoutInput.text.length || currentLine != getLineForOffset(offset + 1))
+    }
+
+/**
+ * Verify that the selection is truly collapsed.
+ *
+ * If the selection is contained within one selectable,
+ * this simply checks if the offsets are equal.
+ *
+ * If the Selection spans multiple selectables, then this will verify that every selected
+ * selectable contains a zero-width selection.
+ */
+internal fun Selection?.isCollapsed(layout: SelectionLayout?): Boolean {
+    this ?: return true
+    layout ?: return true
+
+    // Selection is within one selectable, simply check if the offsets are the same.
+    if (start.selectableId == end.selectableId) {
+        return start.offset == end.offset
+    }
+
+    // check that maxAnchor offset is 0, else the selection cannot be collapsed.
+    val maxAnchor = if (handlesCrossed) start else end
+    if (maxAnchor.offset != 0) {
+        return false
+    }
+
+    // check that the minAnchor offset is equal to the length of the text,
+    // else the selection is not collapsed
+    val minAnchor = if (handlesCrossed) end else start
+    if (layout.firstInfo.textLength != minAnchor.offset) {
+        return false
+    }
+
+    // Every selectable between the min and max must have empty text,
+    // else there is some text selected.
+    var allTextsEmpty = true
+    layout.forEachMiddleInfo {
+        if (it.inputText.isNotEmpty()) {
+            allTextsEmpty = false
+        }
+    }
+
+    return allTextsEmpty
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 2e1e17d..a02f411 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.text.selection
 
+import androidx.annotation.VisibleForTesting
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.gestures.waitForUpOrCancellation
@@ -33,6 +34,8 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.isSpecified
+import androidx.compose.ui.geometry.isUnspecified
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.input.key.KeyEvent
@@ -48,8 +51,10 @@
 import androidx.compose.ui.platform.TextToolbarStatus
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastFold
 import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
 import kotlin.math.absoluteValue
 import kotlin.math.max
 import kotlin.math.min
@@ -209,7 +214,10 @@
         private set
 
     private val shouldShowMagnifier
-        get() = draggingHandle != null && isInTouchMode && isNonEmptySelection()
+        get() = draggingHandle != null && isInTouchMode && !isTriviallyCollapsedSelection()
+
+    @VisibleForTesting
+    internal var previousSelectionLayout: SelectionLayout? = null
 
     init {
         selectionRegistrar.onPositionChangeCallback = { selectableId ->
@@ -229,7 +237,7 @@
                     position
                 )
 
-                if (positionInContainer != null) {
+                if (positionInContainer.isSpecified) {
                     this.isInTouchMode = isInTouchMode
                     startSelection(
                         position = positionInContainer,
@@ -394,8 +402,31 @@
         return Pair(newSelection, subselections)
     }
 
+    /**
+     * Returns whether the start and end anchors are equal.
+     *
+     * It is possible that this returns true, but the selection is still empty because it has
+     * multiple collapsed selections across multiple selectables. To test for that case, use
+     * [isNonEmptySelection].
+     */
+    internal fun isTriviallyCollapsedSelection(): Boolean {
+        val selection = selection ?: return true
+        return selection.start == selection.end
+    }
+
+    /**
+     * Returns whether the selection selects zero characters.
+     *
+     * It is possible that the selection anchors are different but still result in a zero-width
+     * selection. In this case, you may want to still show the selection anchors, but not allow for
+     * a user to try and copy zero characters. To test for whether the anchors are equal, use
+     * [isTriviallyCollapsedSelection].
+     */
     internal fun isNonEmptySelection(): Boolean {
         val selection = selection ?: return false
+        if (selection.start == selection.end) {
+            return false
+        }
 
         var betweenSelectables = false
         selectionRegistrar.sort(requireContainerCoordinates()).fastForEach {
@@ -686,9 +717,9 @@
     private fun convertToContainerCoordinates(
         layoutCoordinates: LayoutCoordinates,
         offset: Offset
-    ): Offset? {
+    ): Offset {
         val coordinates = containerLayoutCoordinates
-        if (coordinates == null || !coordinates.isAttached) return null
+        if (coordinates == null || !coordinates.isAttached) return Offset.Unspecified
         return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
     }
 
@@ -708,10 +739,11 @@
         isStartHandle: Boolean,
         adjustment: SelectionAdjustment
     ) {
+        previousSelectionLayout = null
         updateSelection(
             startHandlePosition = position,
             endHandlePosition = position,
-            previousHandlePosition = null,
+            previousHandlePosition = Offset.Unspecified,
             isStartHandle = isStartHandle,
             adjustment = adjustment
         )
@@ -732,7 +764,7 @@
      */
     internal fun updateSelection(
         newPosition: Offset?,
-        previousPosition: Offset?,
+        previousPosition: Offset,
         isStartHandle: Boolean,
         adjustment: SelectionAdjustment,
     ): Boolean {
@@ -789,46 +821,79 @@
     internal fun updateSelection(
         startHandlePosition: Offset,
         endHandlePosition: Offset,
-        previousHandlePosition: Offset?,
+        previousHandlePosition: Offset,
         isStartHandle: Boolean,
         adjustment: SelectionAdjustment,
     ): Boolean {
         draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
         currentDragPosition = if (isStartHandle) startHandlePosition else endHandlePosition
-        val newSubselections = mutableMapOf<Long, Selection>()
-        var moveConsumed = false
-        val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
-            .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
-                val previousSubSelection =
-                    selectionRegistrar.subselections[selectable.selectableId]
-                val (selection, consumed) = selectable.updateSelection(
-                    startHandlePosition = startHandlePosition,
-                    endHandlePosition = endHandlePosition,
-                    previousHandlePosition = previousHandlePosition,
-                    isStartHandle = isStartHandle,
-                    containerLayoutCoordinates = requireContainerCoordinates(),
-                    adjustment = adjustment,
-                    previousSelection = previousSubSelection,
-                )
 
-                moveConsumed = moveConsumed || consumed
-                selection?.let { newSubselections[selectable.selectableId] = it }
-                merge(mergedSelection, selection)
-            }
+        val selectionLayout = getSelectionLayout(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = previousHandlePosition,
+            isStartHandle = isStartHandle,
+        )
 
-        if (newSelection != selection) {
-            if (isInTouchMode) {
-                hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
-            }
-            selectionRegistrar.subselections = newSubselections
-            onSelectionChange(newSelection)
-            // always consume if selection changed, it is possible that it is false at this
-            // point if selectables were only removed from the selection
-            moveConsumed = true
+        if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
+            return false
         }
-        return moveConsumed
+
+        val newSelection = adjustment.adjust(selectionLayout)
+        if (newSelection != selection) {
+            selectionChanged(selectionLayout, newSelection)
+        }
+        previousSelectionLayout = selectionLayout
+        return true
     }
 
+    private fun getSelectionLayout(
+        startHandlePosition: Offset,
+        endHandlePosition: Offset,
+        previousHandlePosition: Offset,
+        isStartHandle: Boolean,
+    ): SelectionLayout {
+        val containerCoordinates = requireContainerCoordinates()
+        val sortedSelectables = selectionRegistrar.sort(containerCoordinates)
+
+        val idToIndexMap = mutableMapOf<Long, Int>()
+        sortedSelectables.fastForEachIndexed { index, selectable ->
+            idToIndexMap[selectable.selectableId] = index
+        }
+
+        val selectableIdOrderingComparator = compareBy<Long> { idToIndexMap[it] }
+
+        // if previous handle is null, then treat this as a new selection.
+        val previousSelection = if (previousHandlePosition.isUnspecified) null else selection
+        val builder = SelectionLayoutBuilder(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = previousHandlePosition,
+            containerCoordinates = containerCoordinates,
+            isStartHandle = isStartHandle,
+            previousSelection = previousSelection,
+            selectableIdOrderingComparator = selectableIdOrderingComparator,
+        )
+
+        sortedSelectables.fastForEach {
+            it.appendSelectableInfoToBuilder(builder)
+        }
+
+        return builder.build()
+    }
+
+    private fun selectionChanged(selectionLayout: SelectionLayout, newSelection: Selection) {
+        if (shouldPerformHaptics()) {
+            hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+        }
+        selectionRegistrar.subselections = selectionLayout.createSubSelections(newSelection)
+        onSelectionChange(newSelection)
+    }
+
+    @VisibleForTesting
+    internal fun shouldPerformHaptics(): Boolean =
+        isInTouchMode && selectionRegistrar.selectables.fastAny { it.getText().isNotEmpty() }
+
     fun contextMenuOpenAdjustment(position: Offset) {
         val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
         // TODO(b/209483184) the logic should be more complex here, it should check that current
@@ -855,60 +920,71 @@
     manager: SelectionManager,
     magnifierSize: IntSize
 ): Offset {
-    fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {
-        val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
-        val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
-        val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
-        // The end offset is exclusive.
-        val offset = if (isStartHandle) anchor.offset else anchor.offset - 1
-
-        if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
-
-        // The horizontal position doesn't snap to cursor positions but should directly track the
-        // actual drag.
-        val localDragPosition = selectableCoordinates.localPositionOf(
-            containerCoordinates,
-            manager.currentDragPosition!!
-        )
-        val dragX = localDragPosition.x
-        // But it is constrained by the horizontal bounds of the current line.
-        val centerX = selectable.getRangeOfLineContaining(offset).let { line ->
-            val lineMin = selectable.getBoundingBox(line.min)
-            // line.end is exclusive, but we want the bounding box of the actual last character in
-            // the line.
-            val lineMax = selectable.getBoundingBox((line.max - 1).coerceAtLeast(line.min))
-            val minX = minOf(lineMin.left, lineMax.left)
-            val maxX = maxOf(lineMin.right, lineMax.right)
-            dragX.coerceIn(minX, maxX)
-        }
-
-        // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
-        // magnifier actually is). See
-        // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
-        if ((dragX - centerX).absoluteValue > magnifierSize.width / 2) {
-            return Offset.Unspecified
-        }
-
-        // Let the selectable determine the vertical position of the magnifier, since it should be
-        // clamped to the center of text lines.
-        val anchorBounds = selectable.getBoundingBox(offset)
-        val centerY = anchorBounds.center.y
-
-        return containerCoordinates.localPositionOf(
-            sourceCoordinates = selectableCoordinates,
-            relativeToSource = Offset(centerX, centerY)
-        )
-    }
-
     val selection = manager.selection ?: return Offset.Unspecified
     return when (manager.draggingHandle) {
         null -> return Offset.Unspecified
-        Handle.SelectionStart -> getMagnifierCenter(selection.start, isStartHandle = true)
-        Handle.SelectionEnd -> getMagnifierCenter(selection.end, isStartHandle = false)
+        Handle.SelectionStart -> getMagnifierCenter(manager, magnifierSize, selection.start)
+        Handle.SelectionEnd -> getMagnifierCenter(manager, magnifierSize, selection.end)
         Handle.Cursor -> error("SelectionContainer does not support cursor")
     }
 }
 
+private fun getMagnifierCenter(
+    manager: SelectionManager,
+    magnifierSize: IntSize,
+    anchor: AnchorInfo
+): Offset {
+    val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
+    val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
+    val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
+    val offset = anchor.offset
+
+    if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
+
+    // The horizontal position doesn't snap to cursor positions but should directly track the
+    // actual drag.
+    val localDragPosition = selectableCoordinates.localPositionOf(
+        containerCoordinates,
+        manager.currentDragPosition!!
+    )
+    val dragX = localDragPosition.x
+
+    // But it is constrained by the horizontal bounds of the current line.
+    val lineRange = selectable.getRangeOfLineContaining(offset)
+    val textConstrainedX = if (lineRange.collapsed) {
+        // A collapsed range implies the text is empty.
+        // line left and right are equal for this offset, so use either
+        selectable.getLineLeft(offset)
+    } else {
+        val lineStartX = selectable.getLineLeft(lineRange.start)
+        val lineEndX = selectable.getLineRight(lineRange.end - 1)
+        // in RTL/BiDi, lineStartX may be larger than lineEndX
+        val minX = minOf(lineStartX, lineEndX)
+        val maxX = maxOf(lineStartX, lineEndX)
+        dragX.coerceIn(minX, maxX)
+    }
+
+    // selectable couldn't determine horizontals
+    if (textConstrainedX == -1f) return Offset.Unspecified
+
+    // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
+    // magnifier actually is). See
+    // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
+    if ((dragX - textConstrainedX).absoluteValue > magnifierSize.width / 2) {
+        return Offset.Unspecified
+    }
+
+    val lineCenterY = selectable.getCenterYForOffset(offset)
+
+    // selectable couldn't determine the line center
+    if (lineCenterY == -1f) return Offset.Unspecified
+
+    return containerCoordinates.localPositionOf(
+        sourceCoordinates = selectableCoordinates,
+        relativeToSource = Offset(textConstrainedX, lineCenterY)
+    )
+}
+
 private fun isCurrentSelectionEmpty(
     selectable: Selectable,
     selection: Selection
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
index 907c15f..dce40ef 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
@@ -19,11 +19,23 @@
 import androidx.compose.foundation.AtomicLong
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
 
-internal class SelectionRegistrarImpl : SelectionRegistrar {
+internal class SelectionRegistrarImpl private constructor(
+    initialIncrementId: Long
+) : SelectionRegistrar {
+    companion object {
+        val Saver = Saver<SelectionRegistrarImpl, Long>(
+            save = { it.incrementId.get() },
+            restore = { SelectionRegistrarImpl(it) }
+        )
+    }
+
+    constructor() : this(initialIncrementId = 1L)
+
     /**
      * A flag to check if the [Selectable]s have already been sorted.
      */
@@ -54,7 +66,7 @@
      * denote an invalid id.
      * @see SelectionRegistrar.InvalidSelectableId
      */
-    private var incrementId = AtomicLong(1)
+    private var incrementId = AtomicLong(initialIncrementId)
 
     /**
      * The callback to be invoked when the position change was triggered.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionDelegate.kt
deleted file mode 100644
index b59398c..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionDelegate.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.selection
-
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextRange
-
-/**
- * Return selection information for TextField.
- *
- * @param textLayoutResult a result of the text layout.
- * @param rawStartOffset unprocessed start offset calculated directly from input position
- * @param rawEndOffset unprocessed end offset calculated directly from input position
- * @param previousHandleOffset the previous offset of the moving handle. When isStartHandle is
- * true, it's the previous offset of the start handle before the movement, and vice versa.
- * When there isn't a valid previousHandleOffset, previousHandleOffset should be -1.
- * @param previousSelection previous selection result
- * @param isStartHandle true if the start handle is being dragged
- * @param adjustment selection is adjusted according to this param
- *
- * @return selected text range.
- */
-internal fun getTextFieldSelection(
-    textLayoutResult: TextLayoutResult?,
-    rawStartOffset: Int,
-    rawEndOffset: Int,
-    previousHandleOffset: Int,
-    previousSelection: TextRange?,
-    isStartHandle: Boolean,
-    adjustment: SelectionAdjustment
-): TextRange {
-    textLayoutResult?.let {
-        val textRange = TextRange(rawStartOffset, rawEndOffset)
-
-        // When the previous selection is null, it's allowed to have collapsed selection.
-        // So we can ignore the SelectionAdjustment.Character.
-        if (previousSelection == null && adjustment == SelectionAdjustment.Character) {
-            return textRange
-        }
-
-        return adjustment.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = textRange,
-            previousHandleOffset = previousHandleOffset,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-    }
-    return TextRange(0, 0)
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 0093bd8..922fd3a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -159,7 +159,9 @@
         private set
 
     /**
-     * The previous offset of the drag, before selection adjustments.
+     * The previous offset of a drag, before selection adjustments.
+     * Only update when a selection layout change has occurred,
+     * or set to -1 if a new drag begins.
      */
     private var previousRawDragOffset: Int = -1
 
@@ -170,6 +172,11 @@
     private var oldValue: TextFieldValue = TextFieldValue()
 
     /**
+     * The previous [SelectionLayout] where [SelectionLayout.shouldRecomputeSelection] was true.
+     */
+    private var previousSelectionLayout: SelectionLayout? = null
+
+    /**
      * [TextDragObserver] for long press and drag to select in TextField.
      */
     internal val touchSelectionObserver = object : TextDragObserver {
@@ -186,6 +193,7 @@
             // While selecting by long-press-dragging, the "end" of the selection is always the one
             // being controlled by the drag.
             draggingHandle = Handle.SelectionEnd
+            previousRawDragOffset = -1
 
             // ensuring that current action mode (selection toolbar) is invalidated
             hideSelectionToolbar()
@@ -204,29 +212,24 @@
                     enterSelectionMode(showFloatingToolbar = false)
                     hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
                     onValueChange(newValue)
-                    previousRawDragOffset = offset
                 }
             } else {
                 if (value.text.isEmpty()) return
                 enterSelectionMode(showFloatingToolbar = false)
-                state?.layoutResult?.let { layoutResult ->
-                    val offset = layoutResult.getOffsetForPosition(startPoint)
-                    val adjustedStartSelection = updateSelection(
-                        // reset selection, otherwise a previous selection may be used
-                        // as context for creating the next selection
-                        value = value.copy(selection = TextRange.Zero),
-                        transformedStartOffset = offset,
-                        transformedEndOffset = offset,
-                        isStartHandle = false,
-                        adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
-                        isTouchBasedSelection = true,
-                    )
-                    // For touch, set the begin offset to the adjusted selection.
-                    // When char based selection is used, we want to ensure we snap the
-                    // beginning offset to the start word boundary of the first selected word.
-                    dragBeginOffsetInText = adjustedStartSelection.start
-                    previousRawDragOffset = offset
-                }
+                val adjustedStartSelection = updateSelection(
+                    // reset selection, otherwise a previous selection may be used
+                    // as context for creating the next selection
+                    value = value.copy(selection = TextRange.Zero),
+                    currentPosition = startPoint,
+                    isStartOfSelection = true,
+                    isStartHandle = false,
+                    adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
+                    isTouchBasedSelection = true,
+                )
+                // For touch, set the begin offset to the adjusted selection.
+                // When char based selection is used, we want to ensure we snap the
+                // beginning offset to the start word boundary of the first selected word.
+                dragBeginOffsetInText = adjustedStartSelection.start
             }
 
             dragBeginPosition = startPoint
@@ -264,14 +267,12 @@
 
                     updateSelection(
                         value = value,
-                        transformedStartOffset = startOffset,
-                        transformedEndOffset = endOffset,
+                        currentPosition = currentDragPosition!!,
+                        isStartOfSelection = false,
                         isStartHandle = false,
                         adjustment = adjustment,
                         isTouchBasedSelection = true,
-                        allowPreviousSelectionCollapsed = true,
                     )
-                    previousRawDragOffset = endOffset
                 } else {
                     val startOffset = dragBeginOffsetInText ?: layoutResult.getOffsetForPosition(
                         position = dragBeginPosition,
@@ -290,14 +291,12 @@
 
                     updateSelection(
                         value = value,
-                        transformedStartOffset = startOffset,
-                        transformedEndOffset = endOffset,
+                        currentPosition = currentDragPosition!!,
+                        isStartOfSelection = false,
                         isStartHandle = false,
                         adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
                         isTouchBasedSelection = true,
-                        allowPreviousSelectionCollapsed = dragBeginOffsetInText == null,
                     )
-                    previousRawDragOffset = endOffset
                 }
             }
             state?.showFloatingToolbar = false
@@ -316,95 +315,78 @@
 
     internal val mouseSelectionObserver = object : MouseSelectionObserver {
         override fun onExtend(downPosition: Offset): Boolean {
-            state?.layoutResult?.let { layoutResult ->
-                val startOffset = offsetMapping.originalToTransformed(value.selection.start)
-                val clickOffset = layoutResult.getOffsetForPosition(downPosition)
-                updateSelection(
-                    value = value,
-                    transformedStartOffset = startOffset,
-                    transformedEndOffset = clickOffset,
-                    isStartHandle = false,
-                    adjustment = SelectionAdjustment.None,
-                    isTouchBasedSelection = false,
-                )
-                return true
-            }
-            return false
+            // can't update selection without a layoutResult, so don't consume
+            state?.layoutResult ?: return false
+            previousRawDragOffset = -1
+            updateSelection(
+                value = value,
+                currentPosition = downPosition,
+                isStartOfSelection = false,
+                isStartHandle = false,
+                adjustment = SelectionAdjustment.None,
+                isTouchBasedSelection = false,
+            )
+            return true
         }
 
         override fun onExtendDrag(dragPosition: Offset): Boolean {
             if (value.text.isEmpty()) return false
+            // can't update selection without a layoutResult, so don't consume
+            state?.layoutResult ?: return false
 
-            state?.layoutResult?.let { layoutResult ->
-                val startOffset = offsetMapping.originalToTransformed(value.selection.start)
-                val dragOffset =
-                    layoutResult.getOffsetForPosition(
-                        position = dragPosition,
-                        coerceInVisibleBounds = false
-                    )
-
-                updateSelection(
-                    value = value,
-                    transformedStartOffset = startOffset,
-                    transformedEndOffset = dragOffset,
-                    isStartHandle = false,
-                    adjustment = SelectionAdjustment.None,
-                    isTouchBasedSelection = false,
-                )
-                return true
-            }
-            return false
+            updateSelection(
+                value = value,
+                currentPosition = dragPosition,
+                isStartOfSelection = false,
+                isStartHandle = false,
+                adjustment = SelectionAdjustment.None,
+                isTouchBasedSelection = false,
+            )
+            return true
         }
 
         override fun onStart(
             downPosition: Offset,
             adjustment: SelectionAdjustment
         ): Boolean {
+            if (value.text.isEmpty()) return false
+            // can't update selection without a layoutResult, so don't consume
+            state?.layoutResult ?: return false
+
             focusRequester?.requestFocus()
-
             dragBeginPosition = downPosition
-
-            state?.layoutResult?.let { layoutResult ->
-                enterSelectionMode()
-                dragBeginOffsetInText = layoutResult.getOffsetForPosition(downPosition)
-                val clickOffset = layoutResult.getOffsetForPosition(dragBeginPosition)
-                updateSelection(
-                    value = value,
-                    transformedStartOffset = clickOffset,
-                    transformedEndOffset = clickOffset,
-                    isStartHandle = false,
-                    adjustment = adjustment,
-                    isTouchBasedSelection = false,
-                )
-                return true
-            }
-            return false
+            previousRawDragOffset = -1
+            enterSelectionMode()
+            updateSelection(
+                value = value,
+                currentPosition = dragBeginPosition,
+                isStartOfSelection = true,
+                isStartHandle = false,
+                adjustment = adjustment,
+                isTouchBasedSelection = false,
+            )
+            return true
         }
 
         override fun onDrag(dragPosition: Offset, adjustment: SelectionAdjustment): Boolean {
             if (value.text.isEmpty()) return false
+            // can't update selection without a layoutResult, so don't consume
+            state?.layoutResult ?: return false
 
-            state?.layoutResult?.let { layoutResult ->
-                val dragOffset =
-                    layoutResult.getOffsetForPosition(
-                        position = dragPosition,
-                        coerceInVisibleBounds = false
-                    )
-
-                updateSelection(
-                    value = value,
-                    transformedStartOffset = dragBeginOffsetInText!!,
-                    transformedEndOffset = dragOffset,
-                    isStartHandle = false,
-                    adjustment = adjustment,
-                    isTouchBasedSelection = false,
-                )
-                return true
-            }
-            return false
+            updateSelection(
+                value = value,
+                currentPosition = dragPosition,
+                isStartOfSelection = false,
+                isStartHandle = false,
+                adjustment = adjustment,
+                isTouchBasedSelection = false,
+            )
+            return true
         }
 
-        override fun onDragDone() { /* Nothing to do */ }
+        override fun onDragDone() {
+            /* Nothing to do */
+        }
     }
 
     /**
@@ -430,41 +412,25 @@
                 // the composable coordinates.
                 dragBeginPosition = getAdjustedCoordinates(getHandlePosition(isStartHandle))
                 currentDragPosition = dragBeginPosition
+                previousRawDragOffset = -1
                 // Zero out the total distance that being dragged.
                 dragTotalDistance = Offset.Zero
                 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
-                previousRawDragOffset = state?.layoutResult?.value
-                    ?.getOffsetForPosition(currentDragPosition!!) ?: -1
                 state?.showFloatingToolbar = false
             }
 
             override fun onDrag(delta: Offset) {
                 dragTotalDistance += delta
 
-                state?.layoutResult?.value?.let { layoutResult ->
-                    currentDragPosition = dragBeginPosition + dragTotalDistance
-                    val startOffset = if (isStartHandle) {
-                        layoutResult.getOffsetForPosition(currentDragPosition!!)
-                    } else {
-                        offsetMapping.originalToTransformed(value.selection.start)
-                    }
-
-                    val endOffset = if (isStartHandle) {
-                        offsetMapping.originalToTransformed(value.selection.end)
-                    } else {
-                        layoutResult.getOffsetForPosition(currentDragPosition!!)
-                    }
-
-                    updateSelection(
-                        value = value,
-                        transformedStartOffset = startOffset,
-                        transformedEndOffset = endOffset,
-                        isStartHandle = isStartHandle,
-                        adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
-                        isTouchBasedSelection = true, // handle drag infers touch
-                    )
-                    previousRawDragOffset = if (isStartHandle) startOffset else endOffset
-                }
+                currentDragPosition = dragBeginPosition + dragTotalDistance
+                updateSelection(
+                    value = value,
+                    currentPosition = currentDragPosition!!,
+                    isStartOfSelection = false,
+                    isStartHandle = isStartHandle,
+                    adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
+                    isTouchBasedSelection = true, // handle drag infers touch
+                )
                 state?.showFloatingToolbar = false
             }
 
@@ -758,10 +724,11 @@
         state?.layoutResult?.let { layoutResult ->
             val offset = layoutResult.getOffsetForPosition(position)
             if (!value.selection.contains(offset)) {
+                previousRawDragOffset = -1
                 updateSelection(
                     value = value,
-                    transformedStartOffset = offset,
-                    transformedEndOffset = offset,
+                    currentPosition = position,
+                    isStartOfSelection = true,
                     isStartHandle = false,
                     adjustment = SelectionAdjustment.Word,
                     isTouchBasedSelection = false // context menu implies non-touch
@@ -807,8 +774,9 @@
                 )?.y ?: 0f
             val endTop =
                 state?.layoutCoordinates?.localToRoot(
-                    Offset(0f,
-                        it.layoutResult?.value?.getCursorRect(transformedEnd)?.top ?: 0f
+                    Offset(
+                        x = 0f,
+                        y = it.layoutResult?.value?.getCursorRect(transformedEnd)?.top ?: 0f
                     )
                 )?.y ?: 0f
 
@@ -828,52 +796,83 @@
      * Update the text field's selection based on new offsets.
      *
      * @param value the current [TextFieldValue]
-     * @param transformedStartOffset the start offset to use
-     * @param transformedEndOffset the end offset to use
+     * @param currentPosition the current position of the cursor/drag
+     * @param isStartOfSelection whether this is the first updateSelection of a selection gesture.
+     * If true, will ignore any previous selection context.
      * @param isStartHandle whether the start handle is being updated
      * @param adjustment The selection adjustment to use
-     * @param allowPreviousSelectionCollapsed Allow a collapsed selection to be passed to selection
-     * adjustment. In most cases, a collapsed selection should be considered "no previous
-     * selection" for selection adjustment. However, in some cases - like starting a selection in
-     * end padding - a collapsed selection may be necessary context to avoid selection flickering.
+     * @param isTouchBasedSelection Whether this is a touch based selection
      */
     private fun updateSelection(
         value: TextFieldValue,
-        transformedStartOffset: Int,
-        transformedEndOffset: Int,
+        currentPosition: Offset,
+        isStartOfSelection: Boolean,
         isStartHandle: Boolean,
         adjustment: SelectionAdjustment,
         isTouchBasedSelection: Boolean,
-        allowPreviousSelectionCollapsed: Boolean = false,
     ): TextRange {
-        val oldTransformedSelection = TextRange(
+        val layoutResult = state?.layoutResult ?: return TextRange.Zero
+        val previousTransformedSelection = TextRange(
             offsetMapping.originalToTransformed(value.selection.start),
             offsetMapping.originalToTransformed(value.selection.end)
         )
 
-        val newTransformedSelection = getTextFieldSelection(
-            textLayoutResult = state?.layoutResult?.value,
-            rawStartOffset = transformedStartOffset,
-            rawEndOffset = transformedEndOffset,
-            previousHandleOffset = previousRawDragOffset,
-            previousSelection = oldTransformedSelection
-                .takeIf { allowPreviousSelectionCollapsed || !it.collapsed },
-            isStartHandle = isStartHandle,
-            adjustment = adjustment,
+        val currentOffset = layoutResult.getOffsetForPosition(
+            position = currentPosition,
+            coerceInVisibleBounds = false
         )
 
+        val rawStartHandleOffset = if (isStartHandle || isStartOfSelection) currentOffset else
+            previousTransformedSelection.start
+
+        val rawEndHandleOffset = if (!isStartHandle || isStartOfSelection) currentOffset else
+            previousTransformedSelection.end
+
+        val previousSelectionLayout = previousSelectionLayout // for smart cast
+        val rawPreviousHandleOffset = if (
+            isStartOfSelection ||
+            previousSelectionLayout == null ||
+            previousRawDragOffset == -1
+        ) {
+            -1
+        } else {
+            previousRawDragOffset
+        }
+
+        val selectionLayout = getTextFieldSelectionLayout(
+            layoutResult = layoutResult.value,
+            rawStartHandleOffset = rawStartHandleOffset,
+            rawEndHandleOffset = rawEndHandleOffset,
+            rawPreviousHandleOffset = rawPreviousHandleOffset,
+            previousSelectionRange = previousTransformedSelection,
+            isStartOfSelection = isStartOfSelection,
+            isStartHandle = isStartHandle,
+        )
+
+        if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
+            return value.selection
+        }
+
+        this.previousSelectionLayout = selectionLayout
+        previousRawDragOffset = currentOffset
+
+        val newTransformedSelection = adjustment.adjust(selectionLayout)
         val newSelection = TextRange(
-            start = offsetMapping.transformedToOriginal(newTransformedSelection.start),
-            end = offsetMapping.transformedToOriginal(newTransformedSelection.end)
+            start = offsetMapping.transformedToOriginal(newTransformedSelection.start.offset),
+            end = offsetMapping.transformedToOriginal(newTransformedSelection.end.offset)
         )
 
         if (newSelection == value.selection) return value.selection
 
         val onlyChangeIsReversed = newSelection.reversed != value.selection.reversed &&
-            newSelection.run { TextRange(end, start) } == value.selection
+            with(newSelection) { TextRange(end, start) } == value.selection
 
-        // don't haptic if we are using a mouse or if we aren't moving the selection bounds
-        if (isTouchBasedSelection && !onlyChangeIsReversed) {
+        val bothSelectionsCollapsed = newSelection.collapsed && value.selection.collapsed
+        if (isTouchBasedSelection &&
+            value.text.isNotEmpty() &&
+            !onlyChangeIsReversed &&
+            !bothSelectionsCollapsed
+        ) {
             hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
         }
 
@@ -969,31 +968,21 @@
         null -> return Offset.Unspecified
         Handle.Cursor,
         Handle.SelectionStart -> manager.value.selection.start
+
         Handle.SelectionEnd -> manager.value.selection.end
     }
-    var textOffset = manager.offsetMapping.originalToTransformed(rawTextOffset)
+    // If the text hasn't been laid out yet, don't show the magnifier.
     val layoutResult = manager.state?.layoutResult?.value ?: return Offset.Unspecified
     val transformedText = manager.state?.textDelegate?.text ?: return Offset.Unspecified
-    textOffset = textOffset.coerceIn(transformedText.indices)
-    // Center vertically on the current line.
-    // If the text hasn't been laid out yet, don't show the modifier.
-    val offsetCenter = layoutResult.getBoundingBox(textOffset).center
+
+    val textOffset = manager.offsetMapping
+        .originalToTransformed(rawTextOffset)
+        .coerceIn(0, transformedText.length)
 
     val dragX = localDragPosition.x
     val line = layoutResult.getLineForOffset(textOffset)
-    val lineStartOffset = layoutResult.getLineStart(line)
-    val lineEndOffset = layoutResult.getLineEnd(line, visibleEnd = true)
-    val areHandlesCrossed = manager.value.selection.start > manager.value.selection.end
-    val lineStart = layoutResult.getHorizontalPosition(
-        lineStartOffset,
-        isStart = true,
-        areHandlesCrossed = areHandlesCrossed
-    )
-    val lineEnd = layoutResult.getHorizontalPosition(
-        lineEndOffset,
-        isStart = false,
-        areHandlesCrossed = areHandlesCrossed
-    )
+    val lineStart = layoutResult.getLineLeft(line)
+    val lineEnd = layoutResult.getLineRight(line)
     val lineMin = minOf(lineStart, lineEnd)
     val lineMax = maxOf(lineStart, lineEnd)
     val centerX = dragX.coerceIn(lineMin, lineMax)
@@ -1005,5 +994,10 @@
         return Offset.Unspecified
     }
 
-    return Offset(centerX, offsetCenter.y)
+    // Center vertically on the current line.
+    val top = layoutResult.getLineTop(line)
+    val bottom = layoutResult.getLineBottom(line)
+    val centerY = ((bottom - top) / 2) + top
+
+    return Offset(centerX, centerY)
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicSecureTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicSecureTextField.kt
index 566184b9..30c6cc0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicSecureTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicSecureTextField.kt
@@ -27,18 +27,21 @@
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.text.platformDefaultKeyMapping
 import androidx.compose.foundation.text2.input.CodepointTransformation
+import androidx.compose.foundation.text2.input.ImeActionHandler
 import androidx.compose.foundation.text2.input.InputTransformation
 import androidx.compose.foundation.text2.input.TextFieldBuffer
 import androidx.compose.foundation.text2.input.TextFieldCharSequence
 import androidx.compose.foundation.text2.input.TextFieldLineLimits
 import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.TextObfuscationMode
+import androidx.compose.foundation.text2.input.internal.syncTextFieldState
 import androidx.compose.foundation.text2.input.mask
 import androidx.compose.foundation.text2.input.then
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
@@ -57,9 +60,11 @@
 import androidx.compose.ui.semantics.password
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.Density
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
@@ -80,6 +85,269 @@
  * entering secure content. Additionally, some context menu actions like cut, copy, and drag are
  * disabled for added security.
  *
+ * Whenever the user edits the text, [onValueChange] is called with the most up to date state
+ * represented by [String] with which developer is expected to update their state.
+ *
+ * While focused and being edited, the caller temporarily loses _direct_ control of the contents of
+ * the field through the [value] parameter. If an unexpected [value] is passed in during this time,
+ * the contents of the field will _not_ be updated to reflect the value until editing is done. When
+ * editing is done (i.e. focus is lost), the field will be updated to the last [value] received. Use
+ * a [filter] to accept or reject changes during editing. For more direct control of the field
+ * contents use the [BasicTextField2] overload that accepts a [TextFieldState].
+ *
+ * @param value The input [String] text to be shown in the text field.
+ * @param onValueChange The callback that is triggered when the user or the system updates the
+ * text. The updated text is passed as a parameter of the callback. The value passed to the callback
+ * will already have had the [filter] applied.
+ * @param modifier optional [Modifier] for this text field.
+ * @param enabled controls the enabled state of the [BasicTextField2]. When `false`, the text
+ * field will be neither editable nor focusable, the input of the text field will not be selectable.
+ * @param onSubmit Called when the user submits a form either by pressing the action button in the
+ * input method editor (IME), or by pressing the enter key on a hardware keyboard. If the user
+ * submits the form by pressing the action button in the IME, the provided IME action is passed to
+ * the function. If the user submits the form by pressing the enter key on a hardware keyboard,
+ * the defined [imeAction] parameter is passed to the function. Return true to indicate that the
+ * action has been handled completely, which will skip the default behavior, such as hiding the
+ * keyboard for the [ImeAction.Done] action.
+ * @param imeAction The IME action. This IME action is honored by keyboard and may show specific
+ * icons on the keyboard.
+ * @param textObfuscationMode Determines the method used to obscure the input text.
+ * @param keyboardType The keyboard type to be used in this text field. It is set to
+ * [KeyboardType.Password] by default. Use [KeyboardType.NumberPassword] for numerical password
+ * fields.
+ * @param inputTransformation Optional [InputTransformation] that will be used to filter changes to
+ * the [TextFieldState] made by the user. The filter will be applied to changes made by hardware and
+ * software keyboard events, pasting or dropping text, accessibility services, and tests. The filter
+ * will _not_ be applied when changing the [value] programmatically, or when the filter is changed.
+ * If the filter is changed on an existing text field, it will be applied to the next user edit.
+ * the filter will not immediately affect the current [value].
+ * @param textStyle Style configuration for text content that's displayed in the editor.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this TextField. You can create and pass in your own remembered [MutableInteractionSource]
+ * if you want to observe [Interaction]s and customize the appearance / behavior of this TextField
+ * for different [Interaction]s.
+ * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified]
+ * provided, there will be no cursor drawn
+ * @param scrollState Used to manage the horizontal scroll when the input content exceeds the
+ * bounds of the text field. It controls the state of the scroll for the text field.
+ * @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 a cursor or selection around the text. [Density]
+ * scope is the one that was used while creating the given text layout.
+ * @param decorationBox Composable lambda that allows to add decorations around text field, such
+ * as icon, placeholder, helper messages or similar, and automatically increase the hit target area
+ * of the text field. To allow you to control the placement of the inner text field relative to your
+ * decorations, the text field implementation will pass in a framework-controlled composable
+ * parameter "innerTextField" to the decorationBox lambda you provide. You must call
+ * innerTextField exactly once.
+ */
+@ExperimentalFoundationApi
+@Composable
+fun BasicSecureTextField(
+    value: String,
+    onValueChange: (String) -> Unit,
+    modifier: Modifier = Modifier,
+    // TODO(b/297425359) Investigate cleaning up the IME action handling APIs.
+    onSubmit: ImeActionHandler? = null,
+    imeAction: ImeAction = ImeAction.Default,
+    textObfuscationMode: TextObfuscationMode = TextObfuscationMode.RevealLastTyped,
+    keyboardType: KeyboardType = KeyboardType.Password,
+    enabled: Boolean = true,
+    inputTransformation: InputTransformation? = null,
+    textStyle: TextStyle = TextStyle.Default,
+    interactionSource: MutableInteractionSource? = null,
+    cursorBrush: Brush = SolidColor(Color.Black),
+    scrollState: ScrollState = rememberScrollState(),
+    onTextLayout: Density.(getResult: () -> TextLayoutResult?) -> Unit = {},
+    decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+        @Composable { innerTextField -> innerTextField() }
+) {
+    val state = remember {
+        TextFieldState(
+            initialText = value,
+            // Initialize the cursor to be at the end of the field.
+            initialSelectionInChars = TextRange(value.length)
+        )
+    }
+
+    // This is effectively a rememberUpdatedState, but it combines the updated state (text) with
+    // some state that is preserved across updates (selection).
+    var valueWithSelection by remember {
+        mutableStateOf(
+            TextFieldValue(
+                text = value,
+                selection = TextRange(value.length)
+            )
+        )
+    }
+    valueWithSelection = valueWithSelection.copy(text = value)
+
+    BasicSecureTextField(
+        state = state,
+        modifier = modifier.syncTextFieldState(
+            state = state,
+            value = valueWithSelection,
+            onValueChanged = {
+                // Don't fire the callback if only the selection/cursor changed.
+                if (it.text != valueWithSelection.text) {
+                    onValueChange(it.text)
+                }
+                valueWithSelection = it
+            },
+            writeSelectionFromTextFieldValue = false
+        ),
+        onSubmit = onSubmit,
+        imeAction = imeAction,
+        textObfuscationMode = textObfuscationMode,
+        keyboardType = keyboardType,
+        enabled = enabled,
+        inputTransformation = inputTransformation,
+        textStyle = textStyle,
+        interactionSource = interactionSource,
+        cursorBrush = cursorBrush,
+        scrollState = scrollState,
+        onTextLayout = onTextLayout,
+        decorationBox = decorationBox,
+    )
+}
+
+/**
+ * BasicSecureTextField is a new text input component that is still in heavy development.
+ * We strongly advise against using it in production as its API and implementation are currently
+ * unstable. Many essential features such as selection, cursor, gestures, etc. may not work
+ * correctly or may not even exist yet.
+ *
+ * BasicSecureTextField is specifically designed for password entry fields and is a preconfigured
+ * alternative to BasicTextField2. It only supports a single line of content and comes with default
+ * settings for KeyboardOptions, filter, and codepointTransformation that are appropriate for
+ * entering secure content. Additionally, some context menu actions like cut, copy, and drag are
+ * disabled for added security.
+ *
+ * Whenever the user edits the text, [onValueChange] is called with the most up to date state
+ * represented by [TextFieldValue] with which developer is expected to update their state.
+ *
+ * While focused and being edited, the caller temporarily loses _direct_ control of the contents of
+ * the field through the [value] parameter. If an unexpected [value] is passed in during this time,
+ * the contents of the field will _not_ be updated to reflect the value until editing is done. When
+ * editing is done (i.e. focus is lost), the field will be updated to the last [value] received. Use
+ * a [filter] to accept or reject changes during editing. For more direct control of the field
+ * contents use the [BasicTextField2] overload that accepts a [TextFieldState].
+ *
+ * This function ignores the [TextFieldValue.composition] property from [value]. The composition
+ * will, however, be reported in [onValueChange].
+ *
+ * @param value The input [TextFieldValue] specifying the text to be shown in the text field and
+ * the cursor position or selection.
+ * @param onValueChange The callback that is triggered when the user or the system updates the
+ * text, cursor, or selection. The updated [TextFieldValue] is passed as a parameter of the
+ * callback. The value passed to the callback will already have had the [filter] applied.
+ * @param modifier optional [Modifier] for this text field.
+ * @param enabled controls the enabled state of the [BasicTextField2]. When `false`, the text
+ * field will be neither editable nor focusable, the input of the text field will not be selectable.
+ * @param onSubmit Called when the user submits a form either by pressing the action button in the
+ * input method editor (IME), or by pressing the enter key on a hardware keyboard. If the user
+ * submits the form by pressing the action button in the IME, the provided IME action is passed to
+ * the function. If the user submits the form by pressing the enter key on a hardware keyboard,
+ * the defined [imeAction] parameter is passed to the function. Return true to indicate that the
+ * action has been handled completely, which will skip the default behavior, such as hiding the
+ * keyboard for the [ImeAction.Done] action.
+ * @param imeAction The IME action. This IME action is honored by keyboard and may show specific
+ * icons on the keyboard.
+ * @param textObfuscationMode Determines the method used to obscure the input text.
+ * @param keyboardType The keyboard type to be used in this text field. It is set to
+ * [KeyboardType.Password] by default. Use [KeyboardType.NumberPassword] for numerical password
+ * fields.
+ * @param inputTransformation Optional [InputTransformation] that will be used to filter changes to
+ * the [TextFieldState] made by the user. The filter will be applied to changes made by hardware and
+ * software keyboard events, pasting or dropping text, accessibility services, and tests. The filter
+ * will _not_ be applied when changing the [value] programmatically, or when the filter is changed.
+ * If the filter is changed on an existing text field, it will be applied to the next user edit.
+ * the filter will not immediately affect the current [value].
+ * @param textStyle Style configuration for text content that's displayed in the editor.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this TextField. You can create and pass in your own remembered [MutableInteractionSource]
+ * if you want to observe [Interaction]s and customize the appearance / behavior of this TextField
+ * for different [Interaction]s.
+ * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified]
+ * provided, there will be no cursor drawn
+ * @param scrollState Used to manage the horizontal scroll when the input content exceeds the
+ * bounds of the text field. It controls the state of the scroll for the text field.
+ * @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 a cursor or selection around the text. [Density]
+ * scope is the one that was used while creating the given text layout.
+ * @param decorationBox Composable lambda that allows to add decorations around text field, such
+ * as icon, placeholder, helper messages or similar, and automatically increase the hit target area
+ * of the text field. To allow you to control the placement of the inner text field relative to your
+ * decorations, the text field implementation will pass in a framework-controlled composable
+ * parameter "innerTextField" to the decorationBox lambda you provide. You must call
+ * innerTextField exactly once.
+ */
+@ExperimentalFoundationApi
+@Composable
+fun BasicSecureTextField(
+    value: TextFieldValue,
+    onValueChange: (TextFieldValue) -> Unit,
+    modifier: Modifier = Modifier,
+    // TODO(b/297425359) Investigate cleaning up the IME action handling APIs.
+    onSubmit: ImeActionHandler? = null,
+    imeAction: ImeAction = ImeAction.Default,
+    textObfuscationMode: TextObfuscationMode = TextObfuscationMode.RevealLastTyped,
+    keyboardType: KeyboardType = KeyboardType.Password,
+    enabled: Boolean = true,
+    inputTransformation: InputTransformation? = null,
+    textStyle: TextStyle = TextStyle.Default,
+    interactionSource: MutableInteractionSource? = null,
+    cursorBrush: Brush = SolidColor(Color.Black),
+    scrollState: ScrollState = rememberScrollState(),
+    onTextLayout: Density.(getResult: () -> TextLayoutResult?) -> Unit = {},
+    decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+        @Composable { innerTextField -> innerTextField() }
+) {
+    val state = remember {
+        TextFieldState(
+            initialText = value.text,
+            initialSelectionInChars = value.selection
+        )
+    }
+
+    BasicSecureTextField(
+        state = state,
+        modifier = modifier.syncTextFieldState(
+            state = state,
+            value = value,
+            onValueChanged = onValueChange,
+            writeSelectionFromTextFieldValue = true
+        ),
+        onSubmit = onSubmit,
+        imeAction = imeAction,
+        textObfuscationMode = textObfuscationMode,
+        keyboardType = keyboardType,
+        enabled = enabled,
+        inputTransformation = inputTransformation,
+        textStyle = textStyle,
+        interactionSource = interactionSource,
+        cursorBrush = cursorBrush,
+        scrollState = scrollState,
+        onTextLayout = onTextLayout,
+        decorationBox = decorationBox,
+    )
+}
+
+/**
+ * BasicSecureTextField is a new text input component that is still in heavy development.
+ * We strongly advise against using it in production as its API and implementation are currently
+ * unstable. Many essential features such as selection, cursor, gestures, etc. may not work
+ * correctly or may not even exist yet.
+ *
+ * BasicSecureTextField is specifically designed for password entry fields and is a preconfigured
+ * alternative to BasicTextField2. It only supports a single line of content and comes with default
+ * settings for KeyboardOptions, filter, and codepointTransformation that are appropriate for
+ * entering secure content. Additionally, some context menu actions like cut, copy, and drag are
+ * disabled for added security.
+ *
  * @param state [TextFieldState] object that holds the internal state of a [BasicTextField2].
  * @param modifier optional [Modifier] for this text field.
  * @param enabled controls the enabled state of the [BasicTextField2]. When `false`, the text
@@ -131,7 +399,8 @@
 fun BasicSecureTextField(
     state: TextFieldState,
     modifier: Modifier = Modifier,
-    onSubmit: ((ImeAction) -> Boolean)? = null,
+    // TODO(b/297425359) Investigate cleaning up the IME action handling APIs.
+    onSubmit: ImeActionHandler? = null,
     imeAction: ImeAction = ImeAction.Default,
     textObfuscationMode: TextObfuscationMode = TextObfuscationMode.RevealLastTyped,
     keyboardType: KeyboardType = KeyboardType.Password,
@@ -207,7 +476,7 @@
                 keyboardType = keyboardType,
                 imeAction = imeAction
             ),
-            keyboardActions = onSubmit?.let { KeyboardActions(onSubmit = it) }
+            keyboardActions = onSubmit?.let { KeyboardActions(onSubmit = it::onImeAction) }
                 ?: KeyboardActions.Default,
             onTextLayout = onTextLayout,
             codepointTransformation = codepointTransformation,
@@ -314,13 +583,39 @@
 // adopted from PasswordTransformationMethod from Android platform.
 private const val LAST_TYPED_CHARACTER_REVEAL_DURATION_MILLIS = 1500L
 
-private fun KeyboardActions(onSubmit: (ImeAction) -> Boolean) = KeyboardActions(
-    onDone = { if (!onSubmit(ImeAction.Done)) defaultKeyboardAction(ImeAction.Done) },
-    onGo = { if (!onSubmit(ImeAction.Go)) defaultKeyboardAction(ImeAction.Go) },
-    onNext = { if (!onSubmit(ImeAction.Next)) defaultKeyboardAction(ImeAction.Next) },
-    onPrevious = { if (!onSubmit(ImeAction.Previous)) defaultKeyboardAction(ImeAction.Previous) },
-    onSearch = { if (!onSubmit(ImeAction.Search)) defaultKeyboardAction(ImeAction.Search) },
-    onSend = { if (!onSubmit(ImeAction.Send)) defaultKeyboardAction(ImeAction.Send) },
+// TODO(b/297425359) Investigate cleaning up the IME action handling APIs.
+@OptIn(ExperimentalFoundationApi::class)
+private fun KeyboardActions(onSubmit: ImeActionHandler) = KeyboardActions(
+    onDone = {
+        if (!onSubmit.onImeAction(ImeAction.Done)) {
+            defaultKeyboardAction(ImeAction.Done)
+        }
+    },
+    onGo = {
+        if (!onSubmit.onImeAction(ImeAction.Go)) {
+            defaultKeyboardAction(ImeAction.Go)
+        }
+    },
+    onNext = {
+        if (!onSubmit.onImeAction(ImeAction.Next)) {
+            defaultKeyboardAction(ImeAction.Next)
+        }
+    },
+    onPrevious = {
+        if (!onSubmit.onImeAction(ImeAction.Previous)) {
+            defaultKeyboardAction(ImeAction.Previous)
+        }
+    },
+    onSearch = {
+        if (!onSubmit.onImeAction(ImeAction.Search)) {
+            defaultKeyboardAction(ImeAction.Search)
+        }
+    },
+    onSend = {
+        if (!onSubmit.onImeAction(ImeAction.Send)) {
+            defaultKeyboardAction(ImeAction.Send)
+        }
+    },
 )
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/ImeActionHandler.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/ImeActionHandler.kt
new file mode 100644
index 0000000..18eb84c
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/ImeActionHandler.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.text2.input
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.text.input.ImeAction
+
+// TODO(b/297425359) Investigate cleaning up the IME action handling APIs.
+@ExperimentalFoundationApi
+@Stable
+fun interface ImeActionHandler {
+    fun onImeAction(action: ImeAction): Boolean
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/StateSyncingModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/StateSyncingModifier.kt
index 72bd889..4e5a2e3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/StateSyncingModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/StateSyncingModifier.kt
@@ -87,11 +87,10 @@
 @OptIn(ExperimentalFoundationApi::class)
 private class StateSyncingModifierNode(
     private val state: TextFieldState,
-    onValueChanged: (TextFieldValue) -> Unit,
+    private var onValueChanged: (TextFieldValue) -> Unit,
     private val writeSelectionFromTextFieldValue: Boolean,
 ) : Modifier.Node(), ObserverModifierNode, FocusEventModifierNode {
 
-    private var onValueChanged = onValueChanged
     private var isFocused = false
     private var lastValueWhileFocused: TextFieldValue? = null
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifier.kt
index f1f6188..74309cf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldMagnifier.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.text.Handle
-import androidx.compose.foundation.text.selection.getHorizontalPosition
 import androidx.compose.foundation.text.selection.visibleBounds
 import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.internal.TextLayoutState
@@ -88,28 +87,14 @@
         Handle.SelectionStart -> selection.start
         Handle.SelectionEnd -> selection.end
     }
-    // Center vertically on the current line.
+
     // If the text hasn't been laid out yet, don't show the modifier.
     val layoutResult = textLayoutState.layoutResult ?: return Offset.Unspecified
-    val offsetCenter = layoutResult.getBoundingBox(
-        textOffset.coerceIn(textFieldState.text.indices)
-    ).center
 
     val dragX = localDragPosition.x
     val line = layoutResult.getLineForOffset(textOffset)
-    val lineStartOffset = layoutResult.getLineStart(line)
-    val lineEndOffset = layoutResult.getLineEnd(line, visibleEnd = true)
-    val areHandlesCrossed = selection.start > selection.end
-    val lineStart = layoutResult.getHorizontalPosition(
-        lineStartOffset,
-        isStart = true,
-        areHandlesCrossed = areHandlesCrossed
-    )
-    val lineEnd = layoutResult.getHorizontalPosition(
-        lineEndOffset,
-        isStart = false,
-        areHandlesCrossed = areHandlesCrossed
-    )
+    val lineStart = layoutResult.getLineLeft(line)
+    val lineEnd = layoutResult.getLineRight(line)
     val lineMin = minOf(lineStart, lineEnd)
     val lineMax = maxOf(lineStart, lineEnd)
     val centerX = dragX.coerceIn(lineMin, lineMax)
@@ -121,7 +106,12 @@
         return Offset.Unspecified
     }
 
-    var offset = Offset(centerX, offsetCenter.y)
+    // Center vertically on the current line.
+    val top = layoutResult.getLineTop(line)
+    val bottom = layoutResult.getLineBottom(line)
+    val centerY = ((bottom - top) / 2) + top
+
+    var offset = Offset(centerX, centerY)
     textLayoutState.innerTextFieldCoordinates?.takeIf { it.isAttached }?.let { innerCoordinates ->
         offset = offset.coerceIn(innerCoordinates.visibleBounds())
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionState.kt
index 7e75fa5..0db02b3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/selection/TextFieldSelectionState.kt
@@ -23,10 +23,11 @@
 import androidx.compose.foundation.text.DefaultCursorThickness
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.selection.SelectionAdjustment
+import androidx.compose.foundation.text.selection.SelectionLayout
 import androidx.compose.foundation.text.selection.containsInclusive
 import androidx.compose.foundation.text.selection.getAdjustedCoordinates
 import androidx.compose.foundation.text.selection.getSelectionHandleCoordinates
-import androidx.compose.foundation.text.selection.getTextFieldSelection
+import androidx.compose.foundation.text.selection.getTextFieldSelectionLayout
 import androidx.compose.foundation.text.selection.isPrecisePointer
 import androidx.compose.foundation.text.selection.visibleBounds
 import androidx.compose.foundation.text2.input.InputTransformation
@@ -170,6 +171,20 @@
         get() = textLayoutState.innerTextFieldCoordinates?.takeIf { it.isAttached }
 
     /**
+     * The most recent [SelectionLayout] that passed the [SelectionLayout.shouldRecomputeSelection]
+     * check. Provides context to the next selection update such as if the selection is shrinking
+     * or not.
+     */
+    private var previousSelectionLayout: SelectionLayout? = null
+
+    /**
+     * The previous offset of a drag, before selection adjustments.
+     * Only update when a selection layout change has occurred,
+     * or set to -1 if a new drag begins.
+     */
+    private var previousRawDragOffset: Int = -1
+
+    /**
      * State of the cursor handle that includes its visibility and position.
      */
     val cursorHandle by derivedStateOf {
@@ -424,7 +439,6 @@
                     startOffset = index,
                     endOffset = index,
                     isStartHandle = false,
-                    previousHandleOffset = -1, // there is no previous drag.
                     adjustment = SelectionAdjustment.Word,
                 )
                 editAsUser {
@@ -486,7 +500,6 @@
     private suspend fun PointerInputScope.detectTextFieldLongPressAndAfterDrag(
         requestFocus: () -> Unit
     ) {
-        var dragPreviousOffset = -1
         var dragBeginOffsetInText = -1
         var dragBeginPosition: Offset = Offset.Unspecified
         var dragTotalDistance: Offset = Offset.Zero
@@ -509,6 +522,7 @@
 
                 dragBeginPosition = dragStartOffset
                 dragTotalDistance = Offset.Zero
+                previousRawDragOffset = -1
 
                 // Long Press at the blank area, the cursor should show up at the end of the line.
                 if (!textLayoutState.isPositionOnText(dragStartOffset)) {
@@ -520,7 +534,6 @@
                     }
                     showCursorHandle = true
                     showCursorHandleToolbar = true
-                    dragPreviousOffset = offset
                 } else {
                     if (textFieldState.text.isEmpty()) return@onDragStart
                     val offset = textLayoutState.getOffsetForPosition(dragStartOffset)
@@ -534,7 +547,6 @@
                         startOffset = offset,
                         endOffset = offset,
                         isStartHandle = false,
-                        previousHandleOffset = -1, // there is no previous drag.
                         adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
                     )
                     editAsUser {
@@ -545,22 +557,21 @@
                     // When char based selection is used, we want to ensure we snap the
                     // beginning offset to the start word boundary of the first selected word.
                     dragBeginOffsetInText = newSelection.start
-                    dragPreviousOffset = offset
                 }
             },
             onDragEnd = {
                 clearHandleDragging()
-                dragPreviousOffset = -1
                 dragBeginOffsetInText = -1
                 dragBeginPosition = Offset.Unspecified
                 dragTotalDistance = Offset.Zero
+                previousRawDragOffset = -1
             },
             onDragCancel = {
                 clearHandleDragging()
-                dragPreviousOffset = -1
                 dragBeginOffsetInText = -1
                 dragBeginPosition = Offset.Unspecified
                 dragTotalDistance = Offset.Zero
+                previousRawDragOffset = -1
             },
             onDrag = onDrag@{ _, dragAmount ->
                 // selection never started, did not consume any drag
@@ -619,7 +630,6 @@
                     endOffset = endOffset,
                     isStartHandle = false,
                     adjustment = adjustment,
-                    previousHandleOffset = dragPreviousOffset,
                     allowPreviousSelectionCollapsed = false,
                 )
 
@@ -639,7 +649,6 @@
                         selectCharsIn(newSelection)
                     }
                 }
-                dragPreviousOffset = endOffset
                 updateHandleDragging(
                     handle = actingHandle,
                     position = currentDragPosition
@@ -653,13 +662,13 @@
     ) {
         var dragBeginPosition: Offset = Offset.Unspecified
         var dragTotalDistance: Offset = Offset.Zero
-        var previousDragOffset = -1
         val handle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
 
         fun onDragStop() {
             clearHandleDragging()
             dragBeginPosition = Offset.Unspecified
             dragTotalDistance = Offset.Zero
+            previousRawDragOffset = -1
         }
 
         // b/288931376: detectDragGestures do not call onDragCancel when composable is disposed.
@@ -676,11 +685,8 @@
 
                     // Zero out the total distance that being dragged.
                     dragTotalDistance = Offset.Zero
-                    previousDragOffset = if (isStartHandle) {
-                        textFieldState.text.selectionInChars.start
-                    } else {
-                        textFieldState.text.selectionInChars.end
-                    }
+
+                    previousRawDragOffset = -1
                 },
                 onDragEnd = { onDragStop() },
                 onDragCancel = { onDragStop() },
@@ -708,7 +714,6 @@
                         startOffset = startOffset,
                         endOffset = endOffset,
                         isStartHandle = isStartHandle,
-                        previousHandleOffset = previousDragOffset,
                         adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
                     )
                     // Do not allow selection to collapse on itself while dragging selection
@@ -718,7 +723,6 @@
                             selectCharsIn(newSelection)
                         }
                     }
-                    previousDragOffset = if (isStartHandle) startOffset else endOffset
                 }
             )
         } finally {
@@ -1079,7 +1083,7 @@
      * @param textFieldCharSequence the current text editing state
      * @param startOffset the start offset to use
      * @param endOffset the end offset to use
-     * @param isStartHandle whether the start handle is being updated
+     * @param isStartHandle whether the start or end handle is being updated
      * @param adjustment The selection adjustment to use
      * @param allowPreviousSelectionCollapsed Allow a collapsed selection to be passed to selection
      * adjustment. In most cases, a collapsed selection should be considered "no previous
@@ -1092,14 +1096,11 @@
         endOffset: Int,
         isStartHandle: Boolean,
         adjustment: SelectionAdjustment,
-        previousHandleOffset: Int,
         allowPreviousSelectionCollapsed: Boolean = false,
     ): TextRange {
         val newSelection = getTextFieldSelection(
-            textLayoutResult = textLayoutState.layoutResult,
             rawStartOffset = startOffset,
             rawEndOffset = endOffset,
-            previousHandleOffset = previousHandleOffset,
             previousSelection = textFieldCharSequence.selectionInChars
                 .takeIf { allowPreviousSelectionCollapsed || !it.collapsed },
             isStartHandle = isStartHandle,
@@ -1119,6 +1120,44 @@
 
         return newSelection
     }
+
+    private fun getTextFieldSelection(
+        rawStartOffset: Int,
+        rawEndOffset: Int,
+        previousSelection: TextRange?,
+        isStartHandle: Boolean,
+        adjustment: SelectionAdjustment
+    ): TextRange {
+        val layoutResult = textLayoutState.layoutResult ?: return TextRange.Zero
+
+        // When the previous selection is null, it's allowed to have collapsed selection on
+        // TextField. So we can ignore the SelectionAdjustment.Character.
+        if (previousSelection == null && adjustment == SelectionAdjustment.Character) {
+            return TextRange(rawStartOffset, rawEndOffset)
+        }
+
+        val selectionLayout = getTextFieldSelectionLayout(
+            layoutResult = layoutResult,
+            rawStartHandleOffset = rawStartOffset,
+            rawEndHandleOffset = rawEndOffset,
+            rawPreviousHandleOffset = previousRawDragOffset,
+            previousSelectionRange = previousSelection ?: TextRange.Zero,
+            isStartOfSelection = previousSelection == null,
+            isStartHandle = isStartHandle,
+        )
+
+        if (previousSelection != null &&
+            !selectionLayout.shouldRecomputeSelection(previousSelectionLayout)
+        ) {
+            return previousSelection
+        }
+
+        val result = adjustment.adjust(selectionLayout).toTextRange()
+        previousSelectionLayout = selectionLayout
+        previousRawDragOffset = if (isStartHandle) rawStartOffset else rawEndOffset
+
+        return result
+    }
 }
 
 private fun TextRange.reverse() = TextRange(end, start)
@@ -1127,8 +1166,8 @@
     setSelection(range.start, range.end)
 }
 
-private val DEBUG = false
-private val DEBUG_TAG = "TextFieldSelectionState"
+private const val DEBUG = false
+private const val DEBUG_TAG = "TextFieldSelectionState"
 
 private fun logDebug(text: () -> String) {
     if (DEBUG) {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectableInfoTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectableInfoTest.kt
new file mode 100644
index 0000000..5d3d283
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectableInfoTest.kt
@@ -0,0 +1,342 @@
+/*
+ * 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.selection
+
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class SelectableInfoTest {
+    @Test
+    fun verifySimpleParameters() {
+        val text = "hi"
+        val selectableInfo = getSelectableInfo(
+            text = text,
+            selectableId = 1L,
+            slot = 1,
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 1,
+            rawPreviousHandleOffset = -1,
+        )
+
+        assertThat(selectableInfo.selectableId).isEqualTo(1L)
+        assertThat(selectableInfo.slot).isEqualTo(1)
+        assertThat(selectableInfo.rawStartHandleOffset).isEqualTo(0)
+        assertThat(selectableInfo.rawEndHandleOffset).isEqualTo(1)
+        assertThat(selectableInfo.rawPreviousHandleOffset).isEqualTo(-1)
+        assertThat(selectableInfo.inputText).isEqualTo(text)
+        assertThat(selectableInfo.textLength).isEqualTo(2)
+    }
+
+    @Test
+    fun rawCrossedStatus_whenStartGreaterThanEnd_isCrossed() {
+        val selectableInfo = getSelectableInfo(
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 0,
+        )
+
+        assertThat(selectableInfo.rawCrossStatus).isEqualTo(CrossStatus.CROSSED)
+    }
+
+    @Test
+    fun rawCrossedStatus_whenStartLessThanEnd_isNotCrossed() {
+        val selectableInfo = getSelectableInfo(
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 1,
+        )
+
+        assertThat(selectableInfo.rawCrossStatus).isEqualTo(CrossStatus.NOT_CROSSED)
+    }
+
+    @Test
+    fun rawCrossedStatus_whenStartEqualToEnd_isCollapsed() {
+        val selectableInfo = getSelectableInfo(
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 1,
+        )
+
+        assertThat(selectableInfo.rawCrossStatus).isEqualTo(CrossStatus.COLLAPSED)
+    }
+
+    @Test
+    fun shouldRecomputeSelection_whenUnchanged_isFalse() {
+        val info = getSelectableInfo(
+            selectableId = 1L,
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 2,
+        )
+
+        val otherInfo = getSelectableInfo(
+            selectableId = 1L,
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 2,
+        )
+
+        assertThat(info.shouldRecomputeSelection(otherInfo)).isFalse()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_whenSelectableChanged_isTrue() {
+        val info = getSelectableInfo(
+            selectableId = 1L,
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 2,
+        )
+
+        val otherInfo = getSelectableInfo(
+            selectableId = 2L,
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 2,
+        )
+
+        assertThat(info.shouldRecomputeSelection(otherInfo)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_whenStartHandleChanged_isTrue() {
+        val info = getSelectableInfo(
+            selectableId = 1L,
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 2,
+        )
+
+        val otherInfo = getSelectableInfo(
+            selectableId = 1L,
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 2,
+        )
+
+        assertThat(info.shouldRecomputeSelection(otherInfo)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_whenEndHandleChanged_isTrue() {
+        val info = getSelectableInfo(
+            selectableId = 1L,
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 2,
+        )
+
+        val otherInfo = getSelectableInfo(
+            selectableId = 1L,
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 3,
+        )
+
+        assertThat(info.shouldRecomputeSelection(otherInfo)).isTrue()
+    }
+
+    @Test
+    fun toAnchor_nonEmptyLine_matchesInfo() {
+        val offset = 0
+        val selectableId = 1L
+        val info = getSelectableInfo(
+            selectableId = selectableId,
+            rtlRanges = emptyList(),
+            rtlLines = emptySet(),
+        )
+
+        val expected = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = offset,
+            selectableId = selectableId
+        )
+
+        assertThat(info.anchorForOffset(offset)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toAnchor_nonEmptyLine_matchesInfo_rtl() {
+        val offset = 0
+        val selectableId = 1L
+        val info = getSelectableInfo(
+            selectableId = selectableId,
+            rtlRanges = listOf(0..0),
+            rtlLines = emptySet(),
+        )
+
+        val expected = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Rtl,
+            offset = offset,
+            selectableId = selectableId
+        )
+
+        assertThat(info.anchorForOffset(offset)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toAnchor_emptyText_usesParagraphDirection() {
+        val offset = 0
+        val selectableId = 1L
+        val info = getSelectableInfo(
+            text = "",
+            selectableId = selectableId,
+            rtlRanges = emptyList(),
+            rtlLines = setOf(0),
+        )
+
+        val expected = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Rtl,
+            offset = offset,
+            selectableId = selectableId
+        )
+
+        assertThat(info.anchorForOffset(offset)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toAnchor_emptyText_usesParagraphDirection_rtl() {
+        val offset = 0
+        val selectableId = 1L
+        val info = getSelectableInfo(
+            text = "",
+            selectableId = selectableId,
+            rtlRanges = listOf(0..0),
+            rtlLines = emptySet(),
+        )
+
+        val expected = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = offset,
+            selectableId = selectableId
+        )
+
+        assertThat(info.anchorForOffset(offset)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toAnchor_emptyLine_usesParagraphDirection() {
+        val offset = 6
+        val selectableId = 1L
+        val info = getSelectableInfo(
+            text = "hello\n\nhello",
+            selectableId = selectableId,
+            rtlRanges = emptyList(),
+            rtlLines = setOf(1),
+            lineBreaks = listOf(6, 7)
+        )
+
+        val expected = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Rtl,
+            offset = offset,
+            selectableId = selectableId
+        )
+
+        assertThat(info.anchorForOffset(offset)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toAnchor_emptyLine_usesParagraphDirection_rtl() {
+        val offset = 6
+        val selectableId = 1L
+        val info = getSelectableInfo(
+            text = "hello\n\nhello",
+            selectableId = selectableId,
+            rtlRanges = listOf(6..6),
+            rtlLines = emptySet(),
+            lineBreaks = listOf(6, 7)
+        )
+
+        val expected = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = offset,
+            selectableId = selectableId
+        )
+
+        assertThat(info.anchorForOffset(offset)).isEqualTo(expected)
+    }
+
+    @Test
+    fun makeSingleLayoutSelection_notCrossed() {
+        val start = 0
+        val end = 5
+        val selectableId = 1L
+        val info = getSelectableInfo(selectableId = selectableId)
+
+        val expected = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = start,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = end,
+                selectableId = selectableId
+            ),
+            handlesCrossed = false
+        )
+
+        assertThat(info.makeSingleLayoutSelection(start, end)).isEqualTo(expected)
+    }
+
+    @Test
+    fun makeSingleLayoutSelection_crossed() {
+        val start = 5
+        val end = 0
+        val selectableId = 1L
+        val info = getSelectableInfo(selectableId = selectableId)
+
+        val expected = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = start,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = end,
+                selectableId = selectableId
+            ),
+            handlesCrossed = true
+        )
+
+        assertThat(info.makeSingleLayoutSelection(start, end)).isEqualTo(expected)
+    }
+
+    private fun getSelectableInfo(
+        text: String = "hello",
+        selectableId: Long = 1L,
+        slot: Int = 1,
+        rawStartHandleOffset: Int = 0,
+        rawEndHandleOffset: Int = 5,
+        rawPreviousHandleOffset: Int = -1,
+        rtlRanges: List<IntRange> = emptyList(),
+        rtlLines: Set<Int> = emptySet(),
+        wordBoundaries: List<TextRange> = listOf(),
+        lineBreaks: List<Int> = emptyList(),
+    ): SelectableInfo = SelectableInfo(
+        selectableId = selectableId,
+        slot = slot,
+        rawStartHandleOffset = rawStartHandleOffset,
+        rawEndHandleOffset = rawEndHandleOffset,
+        rawPreviousHandleOffset = rawPreviousHandleOffset,
+        textLayoutResult = getTextLayoutResultMock(
+            text = text,
+            rtlCharRanges = rtlRanges,
+            wordBoundaries = wordBoundaries,
+            lineBreaks = lineBreaks,
+            rtlLines = rtlLines,
+        ),
+    )
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustmentTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustmentTest.kt
index 9c6649e..69b2b7f 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustmentTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustmentTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -16,357 +16,503 @@
 
 package androidx.compose.foundation.text.selection
 
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.MultiParagraph
-import androidx.compose.ui.text.TextLayoutInput
-import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.packInts
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.mockito.kotlin.any
-import org.mockito.kotlin.mock
 
 @SmallTest
 @RunWith(JUnit4::class)
 class SelectionAdjustmentTest {
+
     @Test
-    fun adjustment_None_noAdjustment() {
-        val textLayoutResult = mockTextLayoutResult(text = "hello world")
-        val rawSelection = TextRange(0, 5)
-        val adjustedSelection = SelectionAdjustment.None.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = true,
-            previousSelectionRange = null
+    fun none_noAdjustment() {
+        val layout = getSingleSelectionLayoutFake(
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 5,
         )
 
-        assertThat(adjustedSelection).isEqualTo(rawSelection)
+        val actualSelection = SelectionAdjustment.None.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 5)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Character_notCollapsed_noAdjustment() {
-        val textLayoutResult = mockTextLayoutResult(text = "hello world")
-        val rawSelection = TextRange(0, 3)
-        val adjustedSelection = SelectionAdjustment.Character.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = true,
-            previousSelectionRange = null
+    fun none_allowCollapsed() {
+        val layout = getSingleSelectionLayoutFake(
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
         )
 
-        assertThat(adjustedSelection).isEqualTo(rawSelection)
+        val actualSelection = SelectionAdjustment.None.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Character_collapsedNotReversed_returnOneCharSelectionNotReversed() {
-        val textLayoutResult = mockTextLayoutResult(text = "hello")
-        // The end offset is moving towards the start offset, which makes the new raw text range
-        // collapsed.
+    fun none_reversed() {
+        val layout = getSingleSelectionLayoutFake(
+            rawStartHandleOffset = 5,
+            rawEndHandleOffset = 0,
+        )
+
+        val actualSelection = SelectionAdjustment.None.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 5, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun none_multiSelectable() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, slot = 1),
+                getSelectableInfoFake(selectableId = 2L, slot = 3),
+            ),
+            currentInfoIndex = 0,
+            startSlot = 1,
+            endSlot = 3,
+        )
+
+        val actualSelection = SelectionAdjustment.None.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 0,
+            endSelectableId = 2L,
+            endOffset = 5
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun character_notCollapsed_noAdjustment() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 3,
+            isStartHandle = true,
+        )
+
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 3)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun character_collapsedNotReversed_returnOneCharSelectionNotReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello",
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 1,
+            previousSelection = getSelection(startOffset = 1, endOffset = 2),
+        )
+
+        // The end offset is moving towards the start offset,
+        // which makes the new raw text range collapsed.
         // After the adjustment, at least one character should be selected.
-        // Since the previousTextRange is not reversed, the adjusted TextRange should
-        // also be not reversed.
-        // Based the above rules, adjusted text range should be [1, 2)
-        val rawSelection = TextRange(1, 1)
-        val previousSelection = TextRange(1, 2)
-        val isStartHandle = false
+        // Since the previousTextRange is not reversed,
+        // the adjusted TextRange should also be not reversed.
+        // Based the above rules, adjusted text range should be [1, 2).
 
-        val adjustedSelection = SelectionAdjustment.Character.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedSelection).isEqualTo(TextRange(1, 2))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 1, endOffset = 2)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Character_collapsedReversed_returnOneCharSelectionReversed() {
-        val textLayoutResult = mockTextLayoutResult(text = "hello")
-        val rawSelection = TextRange(2, 2)
-        val previousTextRange = TextRange(2, 1)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Character.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousTextRange
+    fun character_collapsedReversed_returnOneCharSelectionReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello",
+            rawStartHandleOffset = 2,
+            rawEndHandleOffset = 2,
+            previousSelection = getSelection(startOffset = 2, endOffset = 1),
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(2, 1))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 2, endOffset = 1)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Character_collapsedNotReversed_returnOneUnicodeSelectionNotReversed() {
-        val textLayoutResult = mockTextLayoutResult(text = "hi\uD83D\uDE00")
-        // After the adjustment, the complete unicode should be selected instead of a single
-        // character that is only part of the unicode.
-        val rawSelection = TextRange(2, 2)
-        val previousSelection = TextRange(2, 4)
-        val isStartHandle = false
-
-        val adjustedSelection = SelectionAdjustment.Character.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
+    fun character_collapsedNotReversed_startBoundary_returnOneCharSelectionNotReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+            previousSelection = getSelection(startOffset = 0, endOffset = 1),
         )
 
-        assertThat(adjustedSelection).isEqualTo(TextRange(2, 4))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 1)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Character_collapsedReversed_returnOneUnicodeSelectionReversed() {
-        val textLayoutResult = mockTextLayoutResult(text = "hi\uD83D\uDE00")
-        val rawSelection = TextRange(4, 4)
-        val previousTextRange = TextRange(4, 2)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Character.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousTextRange
+    fun character_collapsedReversed_startBoundary_returnOneCharSelectionReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+            previousSelection = getSelection(startOffset = 1, endOffset = 0),
+            isStartHandle = true,
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(4, 2))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 1, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Character_collapsedNotReversed_returnOneEmojiSelectionNotReversed() {
-        val textLayoutResult = mockTextLayoutResult(text = "#️⃣sharp")
+    fun character_collapsedNotReversed_endBoundary_returnOneCharSelectionNotReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello",
+            rawStartHandleOffset = 5,
+            rawEndHandleOffset = 5,
+            previousSelection = getSelection(startOffset = 4, endOffset = 5),
+            isStartHandle = true,
+        )
+
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 4, endOffset = 5)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun character_collapsedReversed_endBoundary_returnOneCharSelectionReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello",
+            rawStartHandleOffset = 5,
+            rawEndHandleOffset = 5,
+            previousSelection = getSelection(startOffset = 5, endOffset = 4),
+        )
+
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 5, endOffset = 4)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun character_collapsedNotReversed_returnOneUnicodeSelectionNotReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hi\uD83D\uDE00",
+            rawStartHandleOffset = 2,
+            rawEndHandleOffset = 2,
+            previousSelection = getSelection(startOffset = 2, endOffset = 4),
+        )
+
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 2, endOffset = 4)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun character_collapsedReversed_returnOneUnicodeSelectionReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hi\uD83D\uDE00",
+            rawStartHandleOffset = 4,
+            rawEndHandleOffset = 4,
+            previousSelection = getSelection(startOffset = 4, endOffset = 2),
+        )
+
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 4, endOffset = 2)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun character_collapsedNotReversed_returnOneEmojiSelectionNotReversed() {
         // After the adjustment, the unicode sequence representing the keycap # emoji should be
         // selected instead of a single character/unicode that is only part of the emoji.
-        val rawSelection = TextRange(0, 0)
-        val previousSelection = TextRange(0, 3)
-        val isStartHandle = false
-
-        val adjustedSelection = SelectionAdjustment.Character.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
+        val layout = getSingleSelectionLayoutFake(
+            text = "#️⃣sharp",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+            previousSelection = getSelection(startOffset = 0, endOffset = 3),
         )
 
-        assertThat(adjustedSelection).isEqualTo(TextRange(0, 3))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 3)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Character_collapsedReversed_returnOneEmojiSelectionReversed() {
-        val textLayoutResult = mockTextLayoutResult(text = "#️⃣sharp")
-        val rawSelection = TextRange(3, 3)
-        val previousTextRange = TextRange(3, 0)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Character.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousTextRange
+    fun character_collapsedReversed_returnOneEmojiSelectionReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "#️⃣sharp",
+            rawStartHandleOffset = 3,
+            rawEndHandleOffset = 3,
+            previousSelection = getSelection(startOffset = 3, endOffset = 0),
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(3, 0))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 3, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Word_collapsed() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world",
-            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
-        )
-        val rawSelection = TextRange(1, 1)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
+    fun character_collapsedException_multiText_twoTexts() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                // selection starts at the end of the first text...
+                getSelectableInfoFake(
+                    selectableId = 1L,
+                    slot = 1,
+                    text = "hello",
+                    rawStartHandleOffset = 5,
+                    rawEndHandleOffset = 5,
+                ),
+                // and ends at the beginning of the second text...
+                getSelectableInfoFake(
+                    selectableId = 2L,
+                    slot = 3,
+                    text = "hello",
+                    rawStartHandleOffset = 0,
+                    rawEndHandleOffset = 0,
+                ),
+                // so the entire selection covers 0 characters, and thus is collapsed
+            ),
+            currentInfoIndex = 0,
+            startSlot = 1,
+            endSlot = 3,
+            previousSelection = getSelection(
+                startSelectableId = 1L,
+                startOffset = 5,
+                endSelectableId = 2L,
+                endOffset = 1
+            )
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 5))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 2L,
+            endOffset = 0
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Word_collapsed_onStartBoundary() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world",
-            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
-        )
-        val rawSelection = TextRange(6, 6)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
+    fun character_collapsedException_multiText_threeTexts() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                // selection starts at the end of the first text...
+                getSelectableInfoFake(
+                    selectableId = 1L,
+                    slot = 1,
+                    text = "hello",
+                    rawStartHandleOffset = 5,
+                    rawEndHandleOffset = 5,
+                ),
+                // continues through the second empty text...
+                getSelectableInfoFake(
+                    selectableId = 2L,
+                    slot = 3,
+                    text = "",
+                    rawStartHandleOffset = 0,
+                    rawEndHandleOffset = 0,
+                ),
+                // and ends at the beginning of the third text...
+                getSelectableInfoFake(
+                    selectableId = 3L,
+                    slot = 5,
+                    text = "hello",
+                    rawStartHandleOffset = 0,
+                    rawEndHandleOffset = 0,
+                ),
+                // so the entire selection covers 0 characters, and thus is collapsed
+            ),
+            currentInfoIndex = 0,
+            startSlot = 1,
+            endSlot = 5,
+            previousSelection = getSelection(
+                startSelectableId = 1L,
+                startOffset = 5,
+                endSelectableId = 3L,
+                endOffset = 1
+            )
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(6, 11))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 3L,
+            endOffset = 0
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Word_collapsed_onEndBoundary() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world",
-            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
-        )
-        val rawSelection = TextRange(5, 5)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
+    fun character_collapsedException_previousIsNull() {
+        val selection = getSelection(
+            startOffset = 0,
+            endOffset = 0
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 5))
+        val layout = getSingleSelectionLayoutFake(
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+            rawPreviousHandleOffset = 0,
+            previousSelection = null
+        )
+
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        assertThat(actualSelection).isEqualTo(selection)
     }
 
     @Test
-    fun adjustment_Word_collapsed_zero() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world",
-            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
-        )
-        val rawSelection = TextRange(0, 0)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 5))
-    }
-
-    @Test
-    fun adjustment_Word_collapsed_lastIndex() {
-        val text = "hello world"
-        val textLayoutResult = mockTextLayoutResult(
-            text = text,
-            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
-        )
-        val rawSelection = TextRange(text.lastIndex, text.lastIndex)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(6, 11))
-    }
-
-    @Test
-    fun adjustment_Word_collapsed_textLength() {
-        val text = "hello world"
-        val textLayoutResult = mockTextLayoutResult(
-            text = text,
-            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
-        )
-        val rawSelection = TextRange(text.length, text.length)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(6, 11))
-    }
-
-    @Test
-    fun adjustment_Word_collapsed_emptyString() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun character_collapsedException_emptyText() {
+        val layout = getSingleSelectionLayoutFake(
             text = "",
-            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
-        )
-        val rawSelection = TextRange(0, 0)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 0))
+        val actualSelection = SelectionAdjustment.Character.adjust(layout)
+        val expectedSelection = getSelection(
+            startOffset = 0,
+            endOffset = 0
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Word_notReversed() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun word_collapsed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world",
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 1,
             wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
         )
-        // The adjusted selection should cover the word "hello" and is not reversed.
-        val rawSelection = TextRange(1, 2)
 
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
-        )
-
-        // The raw selection
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 5))
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 5)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Word_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun word_collapsed_onStartBoundary() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world",
+            rawStartHandleOffset = 6,
+            rawEndHandleOffset = 6,
             wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
         )
-        // The raw selection is reversed, so the adjusted selection should cover the word "hello"
-        // and is also reversed.
-        val rawSelection = TextRange(2, 1)
 
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(5, 0))
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 6, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Word_crossWords() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun word_collapsed_onEndBoundary() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world",
+            rawStartHandleOffset = 5,
+            rawEndHandleOffset = 5,
+            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
+        )
+
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 5)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun word_collapsed_zero() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
+        )
+
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 5)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun word_collapsed_lastIndex() {
+        val text = "hello world"
+        val layout = getSingleSelectionLayoutFake(
+            text = text,
+            rawStartHandleOffset = text.lastIndex,
+            rawEndHandleOffset = text.lastIndex,
+            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
+        )
+
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 6, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun word_collapsed_textLength() {
+        val text = "hello world"
+        val layout = getSingleSelectionLayoutFake(
+            text = text,
+            rawStartHandleOffset = text.length,
+            rawEndHandleOffset = text.length,
+            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
+        )
+
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 6, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun word_collapsed_emptyString() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+            wordBoundaries = listOf(TextRange(0, 0))
+        )
+
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun word_notReversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world",
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 2,
+            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
+        )
+
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 5)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun word_reversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world",
+            rawStartHandleOffset = 2,
+            rawEndHandleOffset = 1,
+            wordBoundaries = listOf(TextRange(0, 5), TextRange(6, 11))
+        )
+
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 5, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun word_crossWords() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 4,
+            rawEndHandleOffset = 7,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -375,23 +521,17 @@
             )
         )
 
-        val rawSelection = TextRange(4, 7)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 11))
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Word_crossWords_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun word_crossWords_reversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 7,
+            rawEndHandleOffset = 4,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -400,198 +540,277 @@
             )
         )
 
-        val rawSelection = TextRange(7, 4)
-
-        val adjustedTextRange = SelectionAdjustment.Word.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = false,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(11, 0))
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 11, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Paragraph_collapsed() {
-        val textLayoutResult = mockTextLayoutResult(text = "hello world\nhello world")
-
-        val rawSelection = TextRange(14, 14)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Paragraph.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
+    fun word_multiText() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(
+                    selectableId = 1L,
+                    slot = 1,
+                    text = "hello world",
+                    rawStartHandleOffset = 8,
+                    rawEndHandleOffset = 11,
+                    wordBoundaries = listOf(
+                        TextRange(0, 5),
+                        TextRange(6, 11),
+                    )
+                ),
+                getSelectableInfoFake(
+                    selectableId = 2L,
+                    slot = 3,
+                    text = "hello world",
+                    rawStartHandleOffset = 0,
+                    rawEndHandleOffset = 3,
+                    wordBoundaries = listOf(
+                        TextRange(0, 5),
+                        TextRange(6, 11),
+                    )
+                ),
+            ),
+            currentInfoIndex = 0,
+            startSlot = 1,
+            endSlot = 3,
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(12, 23))
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 6,
+            endSelectableId = 2L,
+            endOffset = 5,
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Paragraph_collapsed_zero() {
-
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world\nhello world\nhello world\nhello world"
-        )
-        val rawSelection = TextRange(0, 0)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Paragraph.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
+    fun word_multiText_reversed() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(
+                    selectableId = 1L,
+                    slot = 1,
+                    text = "hello world",
+                    rawStartHandleOffset = 11,
+                    rawEndHandleOffset = 8,
+                    wordBoundaries = listOf(
+                        TextRange(0, 5),
+                        TextRange(6, 11),
+                    )
+                ),
+                getSelectableInfoFake(
+                    selectableId = 2L,
+                    slot = 3,
+                    text = "hello world",
+                    rawStartHandleOffset = 3,
+                    rawEndHandleOffset = 0,
+                    wordBoundaries = listOf(
+                        TextRange(0, 5),
+                        TextRange(6, 11),
+                    )
+                ),
+            ),
+            currentInfoIndex = 0,
+            startSlot = 3,
+            endSlot = 1,
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 11))
+        val actualSelection = SelectionAdjustment.Word.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 2L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 6,
+            handlesCrossed = true,
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_Paragraph_collapsed_lastIndex() {
-        val text = "hello world\nhello world"
-        val textLayoutResult = mockTextLayoutResult(text = text)
-        val rawSelection = TextRange(text.lastIndex, text.lastIndex)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Paragraph.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(12, 23))
-    }
-
-    @Test
-    fun adjustment_Paragraph_collapsed_textLength() {
-        val text = "hello world\nhello world"
-        val textLayoutResult = mockTextLayoutResult(text = text)
-        val rawSelection = TextRange(text.length, text.length)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Paragraph.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(12, 23))
-    }
-
-    @Test
-    fun adjustment_Paragraph_emptyString() {
-        val textLayoutResult = mockTextLayoutResult(text = "")
-        val rawSelection = TextRange(0, 0)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Paragraph.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 0))
-    }
-
-    @Test
-    fun adjustment_Paragraph_notReversed() {
-        val textLayoutResult = mockTextLayoutResult(text = "hello world\nhello world")
-        // The raw selection is not reversed, so the adjusted selection should cover the word
-        // "hello" and is not reversed either.
-        val rawSelection = TextRange(1, 2)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Paragraph.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 11))
-    }
-
-    @Test
-    fun adjustment_Paragraph_reversed() {
-        val textLayoutResult = mockTextLayoutResult(text = "hello world\nhello world")
-        // The raw selection is reversed, so the adjusted selection should cover the word "hello"
-        // and is also reversed.
-        val rawSelection = TextRange(2, 1)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Paragraph.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(11, 0))
-    }
-
-    @Test
-    fun adjustment_Paragraph_crossParagraph() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world\nhello world\nhello world\nhello world"
-        )
-        val rawSelection = TextRange(13, 26)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.Paragraph.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(12, 35))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_initialSelection() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun paragraph_collapsed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world\nhello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            )
-        )
-        // The previous selection is null, it should use word based
-        // selection in this case.
-        val rawSelection = TextRange(3, 3)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = null
+            rawStartHandleOffset = 14,
+            rawEndHandleOffset = 14,
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 5))
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 12, endOffset = 23)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_expandEndWithinWord() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun paragraph_collapsed_zero() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world\nhello world\nhello world\nhello world",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun paragraph_collapsed_lastIndex() {
+        val text = "hello world\nhello world"
+        val layout = getSingleSelectionLayoutFake(
+            text = text,
+            rawStartHandleOffset = text.lastIndex,
+            rawEndHandleOffset = text.lastIndex,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 12, endOffset = 23)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun paragraph_collapsed_textLength() {
+        val text = "hello world\nhello world"
+        val layout = getSingleSelectionLayoutFake(
+            text = text,
+            rawStartHandleOffset = text.length,
+            rawEndHandleOffset = text.length,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 12, endOffset = 23)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun paragraph_emptyString() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun paragraph_notReversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world\nhello world",
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 2,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun paragraph_reversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world\nhello world",
+            rawStartHandleOffset = 2,
+            rawEndHandleOffset = 1,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 11, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun paragraph_crossParagraph() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world\nhello world\nhello world\nhello world",
+            rawStartHandleOffset = 13,
+            rawEndHandleOffset = 26,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 12, endOffset = 35)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun paragraph_multiText() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(
+                    selectableId = 1L,
+                    slot = 1,
+                    text = "hello world",
+                    rawStartHandleOffset = 8,
+                    rawEndHandleOffset = 11,
+                ),
+                getSelectableInfoFake(
+                    selectableId = 2L,
+                    slot = 3,
+                    text = "hello world",
+                    rawStartHandleOffset = 0,
+                    rawEndHandleOffset = 3,
+                ),
+            ),
+            currentInfoIndex = 0,
+            startSlot = 1,
+            endSlot = 3,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 0,
+            endSelectableId = 2L,
+            endOffset = 11,
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun paragraph_multiText_reversed() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(
+                    selectableId = 1L,
+                    slot = 1,
+                    text = "hello world",
+                    rawStartHandleOffset = 11,
+                    rawEndHandleOffset = 8,
+                ),
+                getSelectableInfoFake(
+                    selectableId = 2L,
+                    slot = 3,
+                    text = "hello world",
+                    rawStartHandleOffset = 3,
+                    rawEndHandleOffset = 0,
+                ),
+            ),
+            currentInfoIndex = 0,
+            startSlot = 3,
+            endSlot = 1,
+        )
+
+        val actualSelection = SelectionAdjustment.Paragraph.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 2L,
+            startOffset = 11,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true,
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_initialSelection() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world hello world",
+            rawStartHandleOffset = 3,
+            rawEndHandleOffset = 3,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -599,27 +818,24 @@
                 TextRange(18, 23)
             )
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        // initial selection just uses SelectionAdjustment.Word.
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 5)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_expandEndWithinWord() {
         // The previous selection is [6, 7) and new selection expand the end to 8. This is
         // considered in-word selection. And it will use character-wise selection
-        val rawSelection = TextRange(6, 8)
-        val previousSelection = TextRange(6, 7)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(6, 8))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandStartWithinWord_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world\nhello world",
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world hello world",
+            rawStartHandleOffset = 6,
+            rawEndHandleOffset = 8,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 6, endOffset = 7),
+            rawPreviousHandleOffset = 7,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -628,25 +844,20 @@
             )
         )
 
-        val rawSelection = TextRange(8, 6)
-        val previousSelection = TextRange(7, 6)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = -1,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(8, 6))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 6, endOffset = 8)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_expandStartWithinWord() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world\nhello world",
+    fun characterWithWordAccelerate_expandStartWithinWord_reversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world hello world",
+            rawStartHandleOffset = 8,
+            rawEndHandleOffset = 6,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 7, endOffset = 6),
+            rawPreviousHandleOffset = 7,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -654,54 +865,23 @@
                 TextRange(18, 23)
             )
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 8, endOffset = 6)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_expandStartWithinWord() {
         // The previous selection is [7, 11) and new selection expand the start to 8. This is
         // considered in-word selection. And it will use character-wise selection
-        val rawSelection = TextRange(8, 11)
-        val previousSelection = TextRange(7, 11)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(8, 11))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandEndWithinWord_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world\nhello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            )
-        )
-
-        val rawSelection = TextRange(11, 8)
-        val previousSelection = TextRange(11, 7)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(11, 8))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandEndOutOfWord() {
-        val textLayoutResult = mockTextLayoutResult(
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 8,
+            rawEndHandleOffset = 11,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 7, endOffset = 11),
+            rawPreviousHandleOffset = 7,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -709,29 +889,47 @@
                 TextRange(18, 23)
             )
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 8, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_expandEndWithinWord_reversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world hello world",
+            rawStartHandleOffset = 11,
+            rawEndHandleOffset = 8,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 11, endOffset = 7),
+            rawPreviousHandleOffset = 7,
+            wordBoundaries = listOf(
+                TextRange(0, 5),
+                TextRange(6, 11),
+                TextRange(12, 17),
+                TextRange(18, 23)
+            )
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 11, endOffset = 8)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_expandEndOutOfWord() {
         // The previous selection is [6, 11) and the new selection expand the end to 12.
         // Because the previous selection end is at word boundary, it will use word selection mode.
         // The end did exceed start of the next word(offset = 12), the adjusted
         // selection end will be 17, which is the end of the next word.
-        val rawSelection = TextRange(6, 12)
-        val previousSelection = TextRange(6, 11)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(6, 17))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandStartOutOfWord_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 6,
+            rawEndHandleOffset = 12,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 6, endOffset = 11),
+            rawPreviousHandleOffset = 11,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -740,25 +938,20 @@
             )
         )
 
-        val rawSelection = TextRange(13, 6)
-        val previousSelection = TextRange(11, 6)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(17, 6))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 6, endOffset = 17)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_expandStartOutOfWord() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun characterWithWordAccelerate_expandStartOutOfWord_reversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 13,
+            rawEndHandleOffset = 6,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 11, endOffset = 6),
+            rawPreviousHandleOffset = 11,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -766,30 +959,26 @@
                 TextRange(18, 23)
             )
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 17, endOffset = 6)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_expandStartOutOfWord() {
         // The previous selection is [6, 11) and the new selection expand the start to 5.
-        // Because the previous selection start is at word boundary, it will use word selection
-        // mode.
-        // The start did exceed the end of the previous word(offset = 5), the
-        // adjusted selection end will be 0, which is the start of the previous word.
-        val rawSelection = TextRange(5, 11)
-        val previousSelection = TextRange(6, 11)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 11))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandEndOutOfWord_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+        // Because the previous selection start is at word boundary,
+        // it will use word selection mode.
+        // The start did exceed the end of the previous word(offset = 5),
+        // the adjusted selection end will be 0, which is the start of the previous word.
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 5,
+            rawEndHandleOffset = 11,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 6, endOffset = 11),
+            rawPreviousHandleOffset = 6,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -798,57 +987,20 @@
             )
         )
 
-        // expands to first word boundary, so will select the first word.
-        val rawSelection = TextRange(11, 5)
-        val previousSelection = TextRange(11, 6)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(11, 0))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_expandEndOutOfWord_exceedThreshold() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun characterWithWordAccelerate_expandEndOutOfWord_reversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            )
-        )
-        // The previous selection is [6, 11) and the new selection expand the end to 15.
-        // Because the previous selection end is at word boundary, it will use word based selection
-        // strategy.
-        // Since the 15 exceed the middle of the next word(offset: 14), the adjusted selection end
-        // will be 17.
-        val rawSelection = TextRange(6, 15)
-        val previousSelection = TextRange(6, 11)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(6, 17))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandStartOutOfWord_exceedThreshold_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world hello world",
+            rawStartHandleOffset = 11,
+            rawEndHandleOffset = 5,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 11, endOffset = 6),
+            rawPreviousHandleOffset = 6,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
@@ -857,219 +1009,121 @@
             )
         )
 
-        val rawSelection = TextRange(15, 6)
-        val previousSelection = TextRange(11, 6)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(17, 6))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 11, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_expandStartOutOfWord_exceedThreshold() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world hello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            )
-        )
-        // The previous selection is [6, 11) and the new selection expand the end to 2.
-        // Because the previous selection end is at word boundary, it will use word based selection
-        // strategy.
-        // Since the 2 exceed the middle of the previous word(offset: 2), the adjusted selection
-        // start will be 0.
-        val rawSelection = TextRange(2, 11)
-        val previousSelection = TextRange(6, 11)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 11))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandEndOutOfWord_exceedThreshold_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world hello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            )
-        )
-
-        val rawSelection = TextRange(11, 2)
-        val previousSelection = TextRange(11, 6)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(11, 0))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandEndToNextLine() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world hello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            ),
-            lineLength = 6
-        )
-        // The text line break is as shown(underscore for space):
+    fun characterWithWordAccelerate_expandEndToNextLine() {
+        // The text line break is as shown (underscore for space):
         //   hello_
         //   world_
         //   hello_
         //   world_
         // The previous selection is [3, 4) and new selection expand the end to 8. Because offset
-        // 8 is at the next line, it will use word based selection strategy. And since 8 exceeds
-        // the middle of the next word(offset: 8), the end will be adjusted to word end: 11.
-        val rawSelection = TextRange(3, 8)
-        val previousSelection = TextRange(3, 4)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(3, 11))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandStartToNextLine_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+        // 8 is at the next line, it will use word based selection strategy
+        // and the end will be adjusted to word end: 11.
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 3,
+            rawEndHandleOffset = 7,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 3, endOffset = 4),
+            rawPreviousHandleOffset = 4,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 6
+            lineBreaks = listOf(6, 12, 18)
         )
 
-        val rawSelection = TextRange(8, 3)
-        val previousSelection = TextRange(4, 3)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(11, 3))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 3, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_expandStartToNextLine() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun characterWithWordAccelerate_expandStartToNextLine_reversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 7,
+            rawEndHandleOffset = 3,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 4, endOffset = 3),
+            rawPreviousHandleOffset = 4,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 6
+            lineBreaks = listOf(6, 12, 18)
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 11, endOffset = 3)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_expandStartToNextLine() {
         // The text line break is as shown(underscore for space):
         //   hello_
         //   world_
         //   hello_
         //   world_
         // The previous selection is [6, 8) and new selection expand the start to 3. Because offset
-        // 3 is at the previous line, it will use word based selection strategy. And because 3
-        // does exceed the end of the previous word(offset: 5), the end will be adjusted to
-        // word start: 0.
-        val rawSelection = TextRange(3, 8)
-        val previousSelection = TextRange(7, 8)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 8))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandEndToNextLine_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+        // 3 is at the previous line, it will use word based selection strategy.
+        // The end will be adjusted to word start: 0.
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 4,
+            rawEndHandleOffset = 8,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 7, endOffset = 8),
+            rawPreviousHandleOffset = 7,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 6
+            lineBreaks = listOf(6, 12, 18)
         )
 
-        // expands into first word so will select first word
-        val rawSelection = TextRange(8, 3)
-        val previousSelection = TextRange(8, 7)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(8, 0))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 8)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_expandEndToNextLine_withinWord() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun characterWithWordAccelerate_expandEndToNextLine_reversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 8,
+            rawEndHandleOffset = 3,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 8, endOffset = 7),
+            rawPreviousHandleOffset = 7,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 8
+            lineBreaks = listOf(6, 12, 18)
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 8, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_expandEndToNextLine_withinWord() {
         // The text line break is as shown:
         //   hello wo
         //   rld hell
@@ -1077,63 +1131,53 @@
         // The previous selection is [3, 7) and the end is expanded to 9, which is the next line.
         // Because end offset is moving between lines, it will use word based selection. In this
         // case the word "world" crosses 2 lines, so the candidate values for the adjusted end
-        // offset are 8(first character of the line) and 11(word end). Since 9 is closer to
-        // 11(word end), the end offset will be adjusted to 11.
-        val rawSelection = TextRange(3, 9)
-        val previousSelection = TextRange(3, 7)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(3, 11))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandStartToNextLine_withinWord_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+        // offset are 8(first character of the line) and 11(word end).
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 3,
+            rawEndHandleOffset = 9,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 3, endOffset = 7),
+            rawPreviousHandleOffset = 7,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 8
+            lineBreaks = listOf(8, 16)
         )
 
-        val rawSelection = TextRange(9, 3)
-        val previousSelection = TextRange(7, 3)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(11, 3))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 3, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_expandStartToNextLine_withinWord() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun characterWithWordAccelerate_expandStartToNextLine_withinWord_reversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 9,
+            rawEndHandleOffset = 3,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 7, endOffset = 3),
+            rawPreviousHandleOffset = 7,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 8
+            lineBreaks = listOf(8, 16)
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 11, endOffset = 3)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_expandStartToNextLine_withinWord() {
         // The text line break is as shown:
         //   hello wo
         //   rld hell
@@ -1144,172 +1188,144 @@
         // case the word "hello" crosses 2 lines. The candidate values for the adjusted start
         // offset are 12(word start) and 16(last character of the line). Since we are expanding
         // back, the end offset will be adjusted to the word start at 12.
-        val rawSelection = TextRange(15, 17)
-        val previousSelection = TextRange(16, 17)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(12, 17))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_expandEndToNextLine_withinWord_reverse() {
-        val textLayoutResult = mockTextLayoutResult(
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 15,
+            rawEndHandleOffset = 17,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 16, endOffset = 17),
+            rawPreviousHandleOffset = 16,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 8
+            lineBreaks = listOf(8, 16)
         )
 
-        // crosses line, then uses word based selection which selects the rest of the word
-        val rawSelection = TextRange(17, 15)
-        val previousSelection = TextRange(17, 16)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(17, 12))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 12, endOffset = 17)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_shrinkEnd() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun characterWithWordAccelerate_expandEndToNextLine_withinWord_reverse() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 17,
+            rawEndHandleOffset = 15,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 17, endOffset = 16),
+            rawPreviousHandleOffset = 16,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
-            )
+            ),
+            lineBreaks = listOf(8, 16)
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 17, endOffset = 12)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_shrinkEnd() {
         // The previous selection is [0, 11) and new selection shrink the end to 8. In this case
         // it will use character based selection strategy.
-        val rawSelection = TextRange(0, 8)
-        val previousSelection = TextRange(0, 11)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(0, 8))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_shrinkStart_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            )
-        )
-
-        val rawSelection = TextRange(8, 0)
-        val previousSelection = TextRange(11, 0)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(8, 0))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_shrinkStart() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world hello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            )
-        )
-        // The previous selection is [0, 8) and new selection shrink the start to 2. In this case
-        // it will use character based selection strategy.
-        val rawSelection = TextRange(2, 8)
-        val previousSelection = TextRange(0, 8)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(2, 8))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_shrinkEnd_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world hello world",
-            wordBoundaries = listOf(
-                TextRange(0, 5),
-                TextRange(6, 11),
-                TextRange(12, 17),
-                TextRange(18, 23)
-            )
-        )
-
-        val rawSelection = TextRange(8, 2)
-        val previousSelection = TextRange(8, 0)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(8, 2))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_shrinkEndToPrevLine() {
-        val textLayoutResult = mockTextLayoutResult(
-            text = "hello world hello world",
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 8,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 0, endOffset = 11),
+            rawPreviousHandleOffset = 11,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 6
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 8)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_shrinkStart_reversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world hello world",
+            rawStartHandleOffset = 8,
+            rawEndHandleOffset = 0,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 11, endOffset = 0),
+            rawPreviousHandleOffset = 11,
+            wordBoundaries = listOf(
+                TextRange(0, 5),
+                TextRange(6, 11),
+                TextRange(12, 17),
+                TextRange(18, 23)
+            ),
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 8, endOffset = 0)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_shrinkStart() {
+        // The previous selection is [0, 8) and new selection shrink the start to 2. In this case
+        // it will use character based selection strategy.
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world hello world",
+            rawStartHandleOffset = 2,
+            rawEndHandleOffset = 8,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 0, endOffset = 8),
+            rawPreviousHandleOffset = 0,
+            wordBoundaries = listOf(
+                TextRange(0, 5),
+                TextRange(6, 11),
+                TextRange(12, 17),
+                TextRange(18, 23)
+            ),
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 2, endOffset = 8)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_shrinkEnd_reversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world hello world",
+            rawStartHandleOffset = 8,
+            rawEndHandleOffset = 2,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 8, endOffset = 0),
+            rawPreviousHandleOffset = 0,
+            wordBoundaries = listOf(
+                TextRange(0, 5),
+                TextRange(6, 11),
+                TextRange(12, 17),
+                TextRange(18, 23)
+            ),
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 8, endOffset = 2)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_shrinkEndToPrevLine() {
         // The text line break is as shown(underscore for space):
         //   hello_
         //   world_
@@ -1318,61 +1334,52 @@
         // The previous selection is [2, 8) and new selection shrink the end to 4. Because offset
         // 4 is at the previous line, it will use word based selection strategy. And the end will
         // be snap to 5.
-        val rawSelection = TextRange(2, 4)
-        val previousSelection = TextRange(2, 8)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(2, 5))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_shrinkStartToPrevLine_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 2,
+            rawEndHandleOffset = 4,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 2, endOffset = 8),
+            rawPreviousHandleOffset = 8,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 6
+            lineBreaks = listOf(6, 12, 18)
         )
 
-        val rawSelection = TextRange(4, 2)
-        val previousSelection = TextRange(8, 2)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(5, 2))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 2, endOffset = 5)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_shrinkStartToNextLine() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun characterWithWordAccelerate_shrinkStartToPrevLine_reversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 4,
+            rawEndHandleOffset = 2,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 8, endOffset = 2),
+            rawPreviousHandleOffset = 8,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 6
+            lineBreaks = listOf(6, 12, 18)
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 5, endOffset = 2)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_shrinkStartToNextLine() {
         // The text line break is as shown(underscore for space):
         //   hello_
         //   world_
@@ -1381,61 +1388,52 @@
         // The previous selection is [2, 8) and new selection shrink the end to 7. Because offset
         // 7 is at the next line, it will use word based selection strategy. And the start will
         // be snap to 6.
-        val rawSelection = TextRange(7, 8)
-        val previousSelection = TextRange(2, 8)
-        val isStartHandle = true
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.start,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(6, 8))
-    }
-
-    @Test
-    fun adjustment_characterWithWordAccelerate_shrinkEndToNextLine_reversed() {
-        val textLayoutResult = mockTextLayoutResult(
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 7,
+            rawEndHandleOffset = 8,
+            isStartHandle = true,
+            previousSelection = getSelection(startOffset = 2, endOffset = 8),
+            rawPreviousHandleOffset = 2,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 6
+            lineBreaks = listOf(6, 12, 18)
         )
 
-        val rawSelection = TextRange(8, 7)
-        val previousSelection = TextRange(8, 2)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
-        )
-
-        assertThat(adjustedTextRange).isEqualTo(TextRange(8, 6))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 6, endOffset = 8)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
     @Test
-    fun adjustment_characterWithWordAccelerate_crossLineSelection_notCollapsed() {
-        val textLayoutResult = mockTextLayoutResult(
+    fun characterWithWordAccelerate_shrinkEndToNextLine_reversed() {
+        val layout = getSingleSelectionLayoutFake(
             text = "hello world hello world",
+            rawStartHandleOffset = 8,
+            rawEndHandleOffset = 7,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 8, endOffset = 2),
+            rawPreviousHandleOffset = 2,
             wordBoundaries = listOf(
                 TextRange(0, 5),
                 TextRange(6, 11),
                 TextRange(12, 17),
                 TextRange(18, 23)
             ),
-            lineLength = 6
+            lineBreaks = listOf(6, 12, 18)
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 8, endOffset = 6)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_crossLineSelection_notCollapsed() {
         // The text line break is as shown(underscore for space):
         //   hello_
         //   world_
@@ -1448,66 +1446,307 @@
         // However, in this specific case the selection start offset is already 6,
         // adjusting the end to 6 will result in a collapsed selection [6, 6). So, it should
         // move the end offset to the other word boundary which is 11 instead.
-        val rawSelection = TextRange(6, 7)
-        val previousSelection = TextRange(6, 15)
-        val isStartHandle = false
-
-        val adjustedTextRange = SelectionAdjustment.CharacterWithWordAccelerate.adjust(
-            textLayoutResult = textLayoutResult,
-            newRawSelectionRange = rawSelection,
-            previousHandleOffset = previousSelection.end,
-            isStartHandle = isStartHandle,
-            previousSelectionRange = previousSelection
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world hello world",
+            rawStartHandleOffset = 6,
+            rawEndHandleOffset = 7,
+            isStartHandle = false,
+            previousSelection = getSelection(startOffset = 6, endOffset = 15),
+            rawPreviousHandleOffset = 15,
+            wordBoundaries = listOf(
+                TextRange(0, 5),
+                TextRange(6, 11),
+                TextRange(12, 17),
+                TextRange(18, 23)
+            ),
+            lineBreaks = listOf(6, 12, 18)
         )
 
-        assertThat(adjustedTextRange).isEqualTo(TextRange(6, 11))
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 6, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 
-    private fun mockTextLayoutResult(
-        text: String,
-        wordBoundaries: List<TextRange> = listOf(),
-        lineLength: Int = text.length
-    ): TextLayoutResult {
-        val multiParagraph = mock<MultiParagraph> {
-            on { getWordBoundary(any()) }.thenAnswer { invocation ->
-                val offset = invocation.arguments[0] as Int
-                val wordBoundary = wordBoundaries.find { offset in it.start..it.end }
-                // Workaround: Mockito doesn't work with inline class now. The packed Long is
-                // equal to TextRange(start, end).
-                packInts(wordBoundary!!.start, wordBoundary.end)
-            }
-
-            on { getLineForOffset(any()) }.thenAnswer { invocation ->
-                val offset = invocation.arguments[0] as Int
-                offset / lineLength
-            }
-
-            on { getLineStart(any()) }.thenAnswer { invocation ->
-                val offset = invocation.arguments[0] as Int
-                offset * lineLength
-            }
-
-            on { getLineEnd(any(), any()) }.thenAnswer { invocation ->
-                val offset = invocation.arguments[0] as Int
-                (offset + 1) * lineLength
-            }
-        }
-
-        return TextLayoutResult(
-            layoutInput = TextLayoutInput(
-                text = AnnotatedString(text = text),
-                style = TextStyle.Default,
-                placeholders = emptyList(),
-                maxLines = Int.MAX_VALUE,
-                softWrap = true,
-                overflow = TextOverflow.Clip,
-                density = Density(1f, 1f),
-                layoutDirection = LayoutDirection.Ltr,
-                fontFamilyResolver = mock(),
-                constraints = mock()
+    @Test
+    fun characterWithWordAccelerate_betweenSlots_usesCurrentIndex() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(
+                    text = "hello\nhello\nhello",
+                    selectableId = 1L,
+                    slot = 1,
+                    rawStartHandleOffset = 8,
+                    rawEndHandleOffset = 11,
+                    lineBreaks = listOf(6, 12),
+                    rawPreviousHandleOffset = 6,
+                    wordBoundaries = listOf(
+                        TextRange(0, 5),
+                        TextRange(6, 11),
+                        TextRange(12, 17),
+                    ),
+                ),
             ),
-            multiParagraph = multiParagraph,
-            size = IntSize.Zero
+            currentInfoIndex = 0,
+            startSlot = 1,
+            endSlot = 2, // below the current text
+            previousSelection = getSelection(startOffset = 6, endOffset = 11),
         )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 6, endOffset = 11)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_betweenSlots_usesCurrentIndex_reversed() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(
+                    text = "hello\nhello\nhello",
+                    selectableId = 1L,
+                    slot = 1,
+                    rawStartHandleOffset = 8,
+                    rawEndHandleOffset = 6,
+                    lineBreaks = listOf(6, 12),
+                    rawPreviousHandleOffset = 6,
+                    wordBoundaries = listOf(
+                        TextRange(0, 5),
+                        TextRange(6, 11),
+                        TextRange(12, 17),
+                    ),
+                ),
+            ),
+            currentInfoIndex = 0,
+            startSlot = 1,
+            endSlot = 0, // above the current text
+            previousSelection = getSelection(startOffset = 11, endOffset = 6),
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 11, endOffset = 6)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_differentSelectable_usesWordBoundary() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(
+                    text = "hello",
+                    selectableId = 1L,
+                    slot = 1,
+                    rawStartHandleOffset = 3,
+                    rawEndHandleOffset = 5,
+                    rawPreviousHandleOffset = 5,
+                    wordBoundaries = listOf(TextRange(0, 5)),
+                ),
+                getSelectableInfoFake(
+                    text = "hello",
+                    selectableId = 2L,
+                    slot = 3,
+                    rawStartHandleOffset = 0,
+                    rawEndHandleOffset = 3,
+                    rawPreviousHandleOffset = 2,
+                    wordBoundaries = listOf(TextRange(0, 5)),
+                ),
+            ),
+            currentInfoIndex = 0,
+            startSlot = 1,
+            endSlot = 3,
+            isStartHandle = true,
+            previousSelection = getSelection(
+                startSelectableId = 2L,
+                startOffset = 2,
+                endSelectableId = 2L,
+                endOffset = 3,
+                handlesCrossed = false,
+            ),
+        )
+
+        // selection goes from the second text at [2, 3] and moves the start handle to the third
+        // offset of the third text. Because it moves texts, it uses word based adjustment.
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 0,
+            endSelectableId = 2L,
+            endOffset = 3,
+            handlesCrossed = false,
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_differentSelectable_usesWordBoundary_reversed() {
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(
+                    text = "hello",
+                    selectableId = 1L,
+                    slot = 1,
+                    rawStartHandleOffset = 5,
+                    rawEndHandleOffset = 3,
+                    rawPreviousHandleOffset = 5,
+                    wordBoundaries = listOf(TextRange(0, 5)),
+                ),
+                getSelectableInfoFake(
+                    text = "hello",
+                    selectableId = 2L,
+                    slot = 3,
+                    rawStartHandleOffset = 3,
+                    rawEndHandleOffset = 0,
+                    rawPreviousHandleOffset = 2,
+                    wordBoundaries = listOf(TextRange(0, 5)),
+                ),
+            ),
+            currentInfoIndex = 0,
+            startSlot = 3,
+            endSlot = 1,
+            isStartHandle = false,
+            previousSelection = getSelection(
+                startSelectableId = 2L,
+                startOffset = 3,
+                endSelectableId = 2L,
+                endOffset = 2,
+                handlesCrossed = true,
+            ),
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(
+            endSelectableId = 1L,
+            endOffset = 0,
+            startSelectableId = 2L,
+            startOffset = 3,
+            handlesCrossed = true,
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_collapsed_usesCorrectCross() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello",
+            rawStartHandleOffset = 5,
+            rawEndHandleOffset = 5,
+            rawPreviousHandleOffset = 5,
+            wordBoundaries = listOf(TextRange(0, 5)),
+            isStartHandle = false,
+            previousSelection = getSelection(
+                startSelectableId = 1L,
+                startOffset = 5,
+                endSelectableId = 2L,
+                endOffset = 0,
+                handlesCrossed = false,
+            ),
+        )
+
+        // The selection goes from a collapsed selection from selectable one to selectable two to
+        // a collapsed selection at the end of selectable one.
+        // Because the end handle goes from selectable two to one, it uses word adjustment.
+        // We want to ensure that the handle cross state updates correctly, since this is a case
+        // of a collapsed cross state from the layout.
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true,
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_multiSelectableCollapsed_usesCorrectCross_reversed() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello",
+            rawStartHandleOffset = 5,
+            rawEndHandleOffset = 5,
+            rawPreviousHandleOffset = 5,
+            wordBoundaries = listOf(TextRange(0, 5)),
+            isStartHandle = true,
+            previousSelection = getSelection(
+                startSelectableId = 2L,
+                startOffset = 0,
+                endSelectableId = 1L,
+                endOffset = 5,
+                handlesCrossed = false,
+            ),
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 0,
+            endSelectableId = 1L,
+            endOffset = 5,
+            handlesCrossed = false,
+        )
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    @Test
+    fun characterWithWordAccelerate_largeOffsetJump_cross_sameAsPrevious() {
+        // emulates a BiDi layout where the offset may jump a large amount.
+        // In this case, a 0-8 selection has its start handle jump to 10.
+        // The previous handle also is set to 10 as this emulates a selection handle performing its
+        // first drag. This results in the old selection being re-used, but the "crossStatus"
+        // changing. The layout thinks it is crossed, while the re-used selection is not.
+        // It should go with the value of selection's handlesCrossed.
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world",
+            rawStartHandleOffset = 10,
+            rawEndHandleOffset = 8,
+            rawPreviousHandleOffset = 10,
+            isStartHandle = true,
+            previousSelection = getSelection(
+                startOffset = 0,
+                endOffset = 8
+            ),
+            wordBoundaries = listOf(
+                TextRange(0, 5),
+                TextRange(6, 11),
+            ),
+            rtlRanges = listOf(0..0, 11..11)
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 0, endOffset = 8)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
+    }
+
+    /*
+     * TODO(b/281587713)
+     *  This is a reproduction of a BiDi case that is currently broken.
+     *  Because the start handle goes from 11 to 1, the selection counts as both expanding and
+     *  the previous offset is on a word boundary, leading to the selection using word adjustment
+     *  for the start handle. We want it to use character adjustment.
+     */
+    @Ignore
+    @Test
+    fun characterWithWordAccelerate_largeOffsetJump_cross_updatesSelection() {
+        val layout = getSingleSelectionLayoutFake(
+            text = "hello world",
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 8,
+            rawPreviousHandleOffset = 11,
+            isStartHandle = true,
+            previousSelection = getSelection(
+                startOffset = 0,
+                endOffset = 8
+            ),
+            wordBoundaries = listOf(
+                TextRange(0, 5),
+                TextRange(6, 11),
+            ),
+            rtlRanges = listOf(0..0, 11..11)
+        )
+
+        val actualSelection = SelectionAdjustment.CharacterWithWordAccelerate.adjust(layout)
+        val expectedSelection = getSelection(startOffset = 1, endOffset = 8)
+        assertThat(actualSelection).isEqualTo(expectedSelection)
     }
 }
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
new file mode 100644
index 0000000..37377dc
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
@@ -0,0 +1,347 @@
+/*
+ * 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.selection
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.MultiParagraph
+import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.packInts
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+
+internal fun getSingleSelectionLayoutFake(
+    text: String = "hello",
+    rawStartHandleOffset: Int = 0,
+    rawEndHandleOffset: Int = 5,
+    rawPreviousHandleOffset: Int = -1,
+    rtlRanges: List<IntRange> = emptyList(),
+    wordBoundaries: List<TextRange> = listOf(),
+    lineBreaks: List<Int> = emptyList(),
+    crossStatus: CrossStatus = when {
+        rawStartHandleOffset < rawEndHandleOffset -> CrossStatus.NOT_CROSSED
+        rawStartHandleOffset > rawEndHandleOffset -> CrossStatus.CROSSED
+        else -> CrossStatus.COLLAPSED
+    },
+    isStartHandle: Boolean = false,
+    previousSelection: Selection? = null,
+    shouldRecomputeSelection: Boolean = true,
+    subSelections: Map<Long, Selection> = emptyMap(),
+): SelectionLayout {
+    return getSelectionLayoutFake(
+        infos = listOf(
+            getSelectableInfoFake(
+                text = text,
+                selectableId = 1,
+                slot = 1,
+                rawStartHandleOffset = rawStartHandleOffset,
+                rawEndHandleOffset = rawEndHandleOffset,
+                rawPreviousHandleOffset = rawPreviousHandleOffset,
+                rtlRanges = rtlRanges,
+                wordBoundaries = wordBoundaries,
+                lineBreaks = lineBreaks,
+            )
+        ),
+        currentInfoIndex = 0,
+        startSlot = 1,
+        endSlot = 1,
+        crossStatus = crossStatus,
+        isStartHandle = isStartHandle,
+        previousSelection = previousSelection,
+        shouldRecomputeSelection = shouldRecomputeSelection,
+        subSelections = subSelections,
+    )
+}
+
+internal fun getTextLayoutResultMock(
+    text: String = "hello",
+    rtlCharRanges: List<IntRange> = emptyList(),
+    rtlLines: Set<Int> = emptySet(),
+    wordBoundaries: List<TextRange> = listOf(),
+    lineBreaks: List<Int> = emptyList(),
+): TextLayoutResult {
+    val annotatedString = AnnotatedString(text)
+
+    val textLayoutInput = TextLayoutInput(
+        text = annotatedString,
+        style = TextStyle.Default,
+        placeholders = emptyList(),
+        maxLines = Int.MAX_VALUE,
+        softWrap = false,
+        overflow = TextOverflow.Visible,
+        density = Density(1f),
+        layoutDirection = LayoutDirection.Ltr,
+        fontFamilyResolver = mock(),
+        constraints = Constraints(0L)
+    )
+
+    fun lineForOffset(offset: Int): Int {
+        var line = 0
+        lineBreaks.fastForEach {
+            if (it > offset) {
+                return line
+            }
+            line++
+        }
+        return line
+    }
+
+    val multiParagraph = mock<MultiParagraph> {
+        on { getBidiRunDirection(any()) }.thenAnswer { invocation ->
+            val offset = invocation.arguments[0] as Int
+            if (rtlCharRanges.any { offset in it })
+                ResolvedTextDirection.Rtl else ResolvedTextDirection.Ltr
+        }
+
+        on { getParagraphDirection(any()) }.thenAnswer { invocation ->
+            val offset = invocation.arguments[0] as Int
+            val line = lineForOffset(offset)
+            if (line in rtlLines) ResolvedTextDirection.Rtl else ResolvedTextDirection.Ltr
+        }
+
+        on { getWordBoundary(any()) }.thenAnswer { invocation ->
+            val offset = invocation.arguments[0] as Int
+            val wordBoundary = wordBoundaries.find { offset in it.start..it.end }
+            // Workaround: Mockito doesn't work with inline class now. The packed Long is
+            // equal to TextRange(start, end).
+            packInts(wordBoundary!!.start, wordBoundary.end)
+        }
+
+        on { getLineForOffset(any()) }.thenAnswer { invocation ->
+            val offset = invocation.arguments[0] as Int
+            lineForOffset(offset)
+        }
+
+        on { getLineStart(any()) }.thenAnswer { invocation ->
+            val lineIndex = invocation.arguments[0] as Int
+            if (lineIndex == 0) 0 else lineBreaks[lineIndex - 1]
+        }
+
+        on { getLineEnd(any(), any()) }.thenAnswer { invocation ->
+            val lineIndex = invocation.arguments[0] as Int
+            if (lineIndex == lineBreaks.size) text.length else lineBreaks[lineIndex] - 1
+        }
+    }
+
+    return TextLayoutResult(textLayoutInput, multiParagraph, IntSize.Zero)
+}
+
+internal fun getSelectableInfoFake(
+    text: String = "hello",
+    selectableId: Long = 1L,
+    slot: Int = 1,
+    rawStartHandleOffset: Int = 0,
+    rawEndHandleOffset: Int = text.length,
+    rawPreviousHandleOffset: Int = -1,
+    rtlRanges: List<IntRange> = emptyList(),
+    wordBoundaries: List<TextRange> = listOf(),
+    lineBreaks: List<Int> = emptyList(),
+): SelectableInfo = SelectableInfo(
+    selectableId = selectableId,
+    slot = slot,
+    rawStartHandleOffset = rawStartHandleOffset,
+    rawEndHandleOffset = rawEndHandleOffset,
+    rawPreviousHandleOffset = rawPreviousHandleOffset,
+    textLayoutResult = getTextLayoutResultMock(
+        text = text,
+        rtlCharRanges = rtlRanges,
+        wordBoundaries = wordBoundaries,
+        lineBreaks = lineBreaks,
+    ),
+)
+
+internal fun getSelectionLayoutFake(
+    infos: List<SelectableInfo>,
+    startSlot: Int,
+    endSlot: Int,
+    currentInfoIndex: Int = 0,
+    crossStatus: CrossStatus = when {
+        startSlot < endSlot -> CrossStatus.NOT_CROSSED
+        startSlot > endSlot -> CrossStatus.CROSSED
+        else -> infos.single().rawCrossStatus
+    },
+    startInfo: SelectableInfo =
+        with(infos) { if (crossStatus == CrossStatus.CROSSED) last() else first() },
+    endInfo: SelectableInfo =
+        with(infos) { if (crossStatus == CrossStatus.CROSSED) first() else last() },
+    firstInfo: SelectableInfo = if (crossStatus == CrossStatus.CROSSED) endInfo else startInfo,
+    lastInfo: SelectableInfo = if (crossStatus == CrossStatus.CROSSED) startInfo else endInfo,
+    middleInfos: List<SelectableInfo> =
+        if (infos.size < 2) emptyList() else infos.subList(1, infos.size - 1),
+    isStartHandle: Boolean = false,
+    previousSelection: Selection? = null,
+    shouldRecomputeSelection: Boolean = true,
+    subSelections: Map<Long, Selection> = emptyMap(),
+): SelectionLayout = FakeSelectionLayout(
+    size = infos.size,
+    crossStatus = crossStatus,
+    startSlot = startSlot,
+    endSlot = endSlot,
+    startInfo = startInfo,
+    endInfo = endInfo,
+    currentInfo = infos[currentInfoIndex],
+    firstInfo = firstInfo,
+    lastInfo = lastInfo,
+    middleInfos = middleInfos,
+    isStartHandle = isStartHandle,
+    previousSelection = previousSelection,
+    shouldRecomputeSelection = shouldRecomputeSelection,
+    subSelections = subSelections,
+)
+
+internal class FakeSelectionLayout(
+    override val size: Int,
+    override val crossStatus: CrossStatus,
+    override val startSlot: Int,
+    override val endSlot: Int,
+    override val startInfo: SelectableInfo,
+    override val endInfo: SelectableInfo,
+    override val currentInfo: SelectableInfo,
+    override val firstInfo: SelectableInfo,
+    override val lastInfo: SelectableInfo,
+    override val isStartHandle: Boolean,
+    override val previousSelection: Selection?,
+    private val middleInfos: List<SelectableInfo>,
+    private val shouldRecomputeSelection: Boolean,
+    private val subSelections: Map<Long, Selection>,
+) : SelectionLayout {
+    override fun createSubSelections(selection: Selection): Map<Long, Selection> = subSelections
+    override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
+        middleInfos.forEach(block)
+    }
+
+    override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
+        shouldRecomputeSelection
+}
+
+internal fun getSelection(
+    startOffset: Int = 0,
+    endOffset: Int = 5,
+    startSelectableId: Long = 1L,
+    endSelectableId: Long = 1L,
+    handlesCrossed: Boolean = startSelectableId == endSelectableId && startOffset > endOffset,
+    startLayoutDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr,
+    endLayoutDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr,
+): Selection = Selection(
+    start = Selection.AnchorInfo(
+        direction = startLayoutDirection,
+        offset = startOffset,
+        selectableId = startSelectableId,
+    ),
+    end = Selection.AnchorInfo(
+        direction = endLayoutDirection,
+        offset = endOffset,
+        selectableId = endSelectableId,
+    ),
+    handlesCrossed = handlesCrossed,
+)
+
+internal class FakeSelectable : Selectable {
+    override var selectableId = 0L
+    var getTextCalledTimes = 0
+    var textToReturn: AnnotatedString? = null
+
+    var rawStartHandleOffset = 0
+    var startHandleDirection = Direction.ON
+    var rawEndHandleOffset = 0
+    var endHandleDirection = Direction.ON
+    var rawPreviousHandleOffset = -1 // -1 = no previous offset
+
+    private val selectableKey = 1L
+    private val fakeSelectAllSelection: Selection = Selection(
+        start = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = 0,
+            selectableId = selectableKey
+        ),
+        end = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = 10,
+            selectableId = selectableKey
+        )
+    )
+
+    override fun appendSelectableInfoToBuilder(builder: SelectionLayoutBuilder) {
+        builder.appendInfo(
+            selectableKey,
+            rawStartHandleOffset,
+            startHandleDirection,
+            rawEndHandleOffset,
+            endHandleDirection,
+            rawPreviousHandleOffset,
+            getTextLayoutResultMock(),
+        )
+    }
+
+    override fun getSelectAllSelection(): Selection {
+        return fakeSelectAllSelection
+    }
+
+    override fun getText(): AnnotatedString {
+        getTextCalledTimes++
+        return textToReturn!!
+    }
+
+    override fun getLayoutCoordinates(): LayoutCoordinates? {
+        return null
+    }
+
+    override fun getHandlePosition(selection: Selection, isStartHandle: Boolean): Offset {
+        return Offset.Zero
+    }
+
+    override fun getBoundingBox(offset: Int): Rect {
+        return Rect.Zero
+    }
+
+    override fun getLineLeft(offset: Int): Float {
+        return 0f
+    }
+
+    override fun getLineRight(offset: Int): Float {
+        return 0f
+    }
+
+    override fun getCenterYForOffset(offset: Int): Float {
+        return 0f
+    }
+
+    override fun getRangeOfLineContaining(offset: Int): TextRange {
+        return TextRange.Zero
+    }
+
+    override fun getLastVisibleOffset(): Int {
+        return 0
+    }
+
+    fun clear() {
+        getTextCalledTimes = 0
+        textToReturn = null
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt
new file mode 100644
index 0000000..16c6157
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionLayoutTest.kt
@@ -0,0 +1,1601 @@
+/*
+ * 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.selection
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.TextRange
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class SelectionLayoutTest {
+    @Test
+    fun layoutBuilderSizeZero_throws() {
+        assertFailsWith(IllegalStateException::class) {
+            buildSelectionLayoutForTest { }
+        }
+    }
+
+    @Test
+    fun singleLayout_verifySimpleParameters() {
+        val selection = getSelection()
+        val layout = getSingleSelectionLayoutForTest(
+            isStartHandle = true,
+            previousSelection = selection,
+        )
+        assertThat(layout.isStartHandle).isTrue()
+        assertThat(layout.previousSelection).isEqualTo(selection)
+    }
+
+    @Test
+    fun layoutBuilder_verifySimpleParameters() {
+        val selection = getSelection()
+        val layout = buildSelectionLayoutForTest(
+            isStartHandle = true,
+            previousSelection = selection,
+        ) {
+            appendInfoForTest()
+        }
+        assertThat(layout.isStartHandle).isTrue()
+        assertThat(layout.previousSelection).isEqualTo(selection)
+    }
+
+    @Test
+    fun singleLayout_sameInfoForAllSelectableInfoFunctions() {
+        val layout = getSingleSelectionLayoutForTest()
+        // since there is only one info, each info function should return the same
+        val info = layout.currentInfo
+        assertThat(layout.startInfo).isSameInstanceAs(info)
+        assertThat(layout.endInfo).isSameInstanceAs(info)
+        assertThat(layout.firstInfo).isSameInstanceAs(info)
+        assertThat(layout.lastInfo).isSameInstanceAs(info)
+    }
+
+    @Test
+    fun size_singleLayout_returnsOne() {
+        val selection = getSingleSelectionLayoutForTest()
+        assertThat(selection.size).isEqualTo(1)
+    }
+
+    @Test
+    fun size_layoutBuilderSizeOne_returnsOne() {
+        val selection = buildSelectionLayoutForTest {
+            appendInfoForTest()
+        }
+        assertThat(selection.size).isEqualTo(1)
+    }
+
+    @Test
+    fun size_layoutBuilderSizeMoreThanOne_returnsSize() {
+        val selection = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(selection.size).isEqualTo(3)
+    }
+
+    @Test
+    fun startSlot_singleLayout_equalsOnlyInfo() {
+        val layout = getSingleSelectionLayoutForTest()
+        // when there is only one info, slot doesn't matter
+        // so, ensure that the slot is equal to the only info's slot
+        assertThat(layout.startSlot).isEqualTo(layout.currentInfo.slot)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onBefore_equalsZero() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(0)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onFirst_equalsOne() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(1)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onMiddle_equalsTwo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(2)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onLast_equalsThree() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(3)
+    }
+
+    @Test
+    fun startSlot_layoutBuilder_onAfter_equalsFour() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startSlot).isEqualTo(4)
+    }
+
+    @Test
+    fun endSlot_singleLayout_equalsOnlyInfo() {
+        val layout = getSingleSelectionLayoutForTest()
+        // when there is only one info, slot doesn't matter
+        // so, ensure that the slot is equal to the only info's slot
+        assertThat(layout.endSlot).isEqualTo(layout.currentInfo.slot)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onBefore_equalsZero() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.BEFORE,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(0)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onFirst_equalsOne() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(1)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onMiddle_equalsTwo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(2)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onLast_equalsThree() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(3)
+    }
+
+    @Test
+    fun endSlot_layoutBuilder_onAfter_equalsFour() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+        }
+        assertThat(layout.endSlot).isEqualTo(4)
+    }
+
+    @Test
+    fun crossStatus_singleLayout_collapsed() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 0,
+        )
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.COLLAPSED)
+    }
+
+    @Test
+    fun crossStatus_singleLayout_crossed() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 1,
+            rawEndHandleOffset = 0,
+        )
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.CROSSED)
+    }
+
+    @Test
+    fun crossStatus_singleLayout_notCrossed() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 0,
+            rawEndHandleOffset = 1,
+        )
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.NOT_CROSSED)
+    }
+
+    @Test
+    fun crossStatus_layoutBuilder_collapsed() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.ON,
+                rawStartHandleOffset = 0,
+                rawEndHandleOffset = 0,
+            )
+        }
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.COLLAPSED)
+    }
+
+    @Test
+    fun crossStatus_layoutBuilder_crossed() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.CROSSED)
+    }
+
+    @Test
+    fun crossStatus_layoutBuilder_notCrossed() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.crossStatus).isEqualTo(CrossStatus.NOT_CROSSED)
+    }
+
+    // No startInfo test for singleLayout because it is covered in
+    // singleLayout_sameInfoForAllSelectableInfoFunctions
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotZero_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotOne_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotTwo_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotThree_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun startInfo_layoutBuilder_onSlotFour_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.startInfo.selectableId).isEqualTo(2L)
+    }
+
+    // No endInfo test for singleLayout because it is covered in
+    // singleLayout_sameInfoForAllSelectableInfoFunctions
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotZero_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.BEFORE,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotOne_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotTwo_equalsFirstInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotThree_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun endInfo_layoutBuilder_onSlotFour_equalsSecondInfo() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+        }
+        assertThat(layout.endInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun currentInfo_layoutBuilder_currentInfo_startHandle_equalsFirst() {
+        val layout = buildSelectionLayoutForTest(isStartHandle = true) {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.currentInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun currentInfo_layoutBuilder_endHandle_equalsSecond() {
+        val layout = buildSelectionLayoutForTest(isStartHandle = false) {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.currentInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun firstInfo_layoutBuilder_notCrossed_equalsFirst() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.firstInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun firstInfo_layoutBuilder_crossed_equalsFirst() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.firstInfo.selectableId).isEqualTo(1L)
+    }
+
+    @Test
+    fun lastInfo_layoutBuilder_notCrossed_equalsSecond() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.lastInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun lastInfo_layoutBuilder_crossed_equalsSecond() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.BEFORE,
+            )
+        }
+        assertThat(layout.lastInfo.selectableId).isEqualTo(2L)
+    }
+
+    @Test
+    fun middleInfos_singleLayout_isEmpty() {
+        val layout = getSingleSelectionLayoutForTest()
+        val infoList = mutableListOf<SelectableInfo>()
+        layout.forEachMiddleInfo { infoList += it }
+        assertThat(infoList).isEmpty()
+    }
+
+    @Test
+    fun middleInfos_layoutBuilder_twoInfos_isEmpty() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+
+        val infoList = mutableListOf<SelectableInfo>()
+        layout.forEachMiddleInfo { infoList += it }
+        assertThat(infoList).isEmpty()
+    }
+
+    @Test
+    fun middleInfos_layoutBuilder_threeInfos_containsOneElement() {
+        val info: SelectableInfo
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            info = appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        val infoList = mutableListOf<SelectableInfo>()
+        layout.forEachMiddleInfo { infoList += it }
+        assertThat(infoList).containsExactly(info)
+    }
+
+    @Test
+    fun middleInfos_layoutBuilder_fourInfos_containsTwoElements() {
+        val infoOne: SelectableInfo
+        val infoTwo: SelectableInfo
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            infoOne = appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            infoTwo = appendInfoForTest(
+                selectableId = 3L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 4L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        val infoList = mutableListOf<SelectableInfo>()
+        layout.forEachMiddleInfo { infoList += it }
+        assertThat(infoList).containsExactly(infoOne, infoTwo).inOrder()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_otherNull_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(null)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_otherMulti_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        val otherLayout = buildSelectionLayoutForTest {
+            appendInfoForTest()
+            appendInfoForTest()
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_differentHandle_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            isStartHandle = true,
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        val otherLayout = getSingleSelectionLayoutForTest(
+            isStartHandle = false,
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_differentInfo_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 0,
+            previousSelection = getSelection()
+        )
+        val otherLayout = getSingleSelectionLayoutForTest(
+            rawStartHandleOffset = 1,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_noPreviousSelection_returnsTrue() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+        )
+        val otherLayout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_sameLayout_returnsFalse() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(layout)).isFalse()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_singleLayout_equalLayout_returnsFalse() {
+        val layout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        val otherLayout = getSingleSelectionLayoutForTest(
+            rawPreviousHandleOffset = 5,
+            previousSelection = getSelection()
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isFalse()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_otherNull_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(null)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_otherSingle_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = getSingleSelectionLayoutForTest(
+            previousSelection = getSelection(),
+            rawPreviousHandleOffset = 5
+        )
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentStartSlot_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentEndSlot_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                rawEndHandleOffset = 5,
+                rawPreviousHandleOffset = 5,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentHandle_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            isStartHandle = true,
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection(),
+            isStartHandle = false
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentSize_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_differentInfo_returnsTrue() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 4, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isTrue()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_sameLayout_returnsFalse() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(layout)).isFalse()
+    }
+
+    @Test
+    fun shouldRecomputeSelection_layoutBuilder_equalLayout_returnsFalse() {
+        val layout = buildSelectionLayoutForTest(
+            previousSelection = getSelection()
+        ) {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        val otherLayout = buildSelectionLayoutForTest {
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+            appendInfoForTest(rawEndHandleOffset = 5, rawPreviousHandleOffset = 5)
+        }
+        assertThat(layout.shouldRecomputeSelection(otherLayout)).isFalse()
+    }
+
+    @Test
+    fun createSubSelections_singleLayout_missNonCrossedSelection_throws() {
+        val layout = getSingleSelectionLayoutForTest()
+        val selection = getSelection(startOffset = 1, endOffset = 0, handlesCrossed = false)
+        assertFailsWith(IllegalStateException::class) {
+            layout.createSubSelections(selection)
+        }
+    }
+
+    @Test
+    fun createSubSelections_singleLayout_missCrossedSelection_throws() {
+        val layout = getSingleSelectionLayoutForTest()
+        val selection = getSelection(startOffset = 0, endOffset = 1, handlesCrossed = true)
+        assertFailsWith(IllegalStateException::class) {
+            layout.createSubSelections(selection)
+        }
+    }
+
+    @Test
+    fun createSubSelections_singleLayout_validSelection_returnsInputSelection() {
+        val layout = getSingleSelectionLayoutForTest()
+        val selection = getSelection()
+        val actual = layout.createSubSelections(selection)
+        assertThat(actual).hasSize(1)
+        // We don't care about the selectableId since it isn't used anyways
+        assertThat(actual.toList().single().second).isEqualTo(selection)
+    }
+
+    @Test
+    fun createSubSelections_builtSingleLayout_validSelection_returnsInputSelection() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(selectableId = 1L)
+        }
+        val selection = getSelection()
+        assertThat(layout.createSubSelections(selection)).containsExactly(1L, selection)
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_missNonCrossedSingleSelection_throws() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(selectableId = 1L)
+        }
+        val selection = getSelection(startOffset = 1, endOffset = 0, handlesCrossed = false)
+        assertFailsWith(IllegalStateException::class) {
+            layout.createSubSelections(selection)
+        }
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_missCrossedSingleSelection_throws() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(selectableId = 1L)
+        }
+        val selection = getSelection(startOffset = 0, endOffset = 1, handlesCrossed = true)
+        assertFailsWith(IllegalStateException::class) {
+            layout.createSubSelections(selection)
+        }
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_selectionInOneSelectable_returnsInputSelection() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(selectableId = 1L)
+            appendInfoForTest(selectableId = 2L)
+        }
+        val selection = getSelection(startSelectableId = 2L, endSelectableId = 2L)
+        assertThat(layout.createSubSelections(selection)).containsExactly(2L, selection)
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_selectionInTwoSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        val selection = getSelection(startSelectableId = 1L, endSelectableId = 2L)
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
+            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_selectionInThreeSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        val selection = getSelection(startSelectableId = 1L, endSelectableId = 3L)
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
+            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
+            3L, getSelection(startSelectableId = 3L, endSelectableId = 3L),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_selectionInFourSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.AFTER,
+            )
+            appendInfoForTest(
+                selectableId = 4L,
+                startHandleDirection = Direction.BEFORE,
+                endHandleDirection = Direction.ON,
+            )
+        }
+        val selection = getSelection(startSelectableId = 1L, endSelectableId = 4L)
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L, getSelection(startSelectableId = 1L, endSelectableId = 1L),
+            2L, getSelection(startSelectableId = 2L, endSelectableId = 2L),
+            3L, getSelection(startSelectableId = 3L, endSelectableId = 3L),
+            4L, getSelection(startSelectableId = 4L, endSelectableId = 4L),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_crossedSelectionInOneSelectable() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+        }
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true
+        )
+        assertThat(layout.createSubSelections(selection)).containsExactly(1L, selection)
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_crossedSelectionInTwoSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+        }
+        val selection = getSelection(
+            startSelectableId = 2L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true
+        )
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L,
+            getSelection(
+                startSelectableId = 1L,
+                endSelectableId = 1L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            2L,
+            getSelection(
+                startSelectableId = 2L,
+                endSelectableId = 2L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_crossedSelectionInThreeSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+        }
+        val selection = getSelection(
+            startSelectableId = 3L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true
+        )
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L,
+            getSelection(
+                startSelectableId = 1L,
+                endSelectableId = 1L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            2L,
+            getSelection(
+                startSelectableId = 2L,
+                endSelectableId = 2L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            3L,
+            getSelection(
+                startSelectableId = 3L,
+                endSelectableId = 3L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+        )
+    }
+
+    @Test
+    fun createSubSelections_layoutBuilder_crossedSelectionInFourSelectables() {
+        val layout = buildSelectionLayoutForTest {
+            appendInfoForTest(
+                selectableId = 1L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.ON,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 2L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 3L,
+                startHandleDirection = Direction.AFTER,
+                endHandleDirection = Direction.BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+            appendInfoForTest(
+                selectableId = 4L,
+                startHandleDirection = Direction.ON,
+                endHandleDirection = Direction.BEFORE,
+                rawStartHandleOffset = 5,
+                rawEndHandleOffset = 0,
+            )
+        }
+        val selection = getSelection(
+            startSelectableId = 4L,
+            startOffset = 5,
+            endSelectableId = 1L,
+            endOffset = 0,
+            handlesCrossed = true
+        )
+        assertThat(layout.createSubSelections(selection)).containsExactly(
+            1L,
+            getSelection(
+                startSelectableId = 1L,
+                endSelectableId = 1L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            2L,
+            getSelection(
+                startSelectableId = 2L,
+                endSelectableId = 2L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            3L,
+            getSelection(
+                startSelectableId = 3L,
+                endSelectableId = 3L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+            4L,
+            getSelection(
+                startSelectableId = 4L,
+                endSelectableId = 4L,
+                startOffset = 5,
+                endOffset = 0,
+                handlesCrossed = true
+            ),
+        )
+    }
+
+    @Test
+    fun selection_isCollapsed_nullSelection_returnsTrue() {
+        assertThat(null.isCollapsed(getSingleSelectionLayoutFake())).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_nullLayout_returnsTrue() {
+        assertThat(getSelection().isCollapsed(null)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_singleLayout_empty_returnsTrue() {
+        val selection = getSelection(startOffset = 0, endOffset = 0)
+        val layout = getSingleSelectionLayoutFake(text = "")
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_singleLayout_collapsed_returnsTrue() {
+        val selection = getSelection(startOffset = 0, endOffset = 0)
+        val layout = getSingleSelectionLayoutFake(text = "hello")
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_singleLayout_notCollapsed_returnsFalse() {
+        val selection = getSelection(startOffset = 0, endOffset = 5)
+        val layout = getSingleSelectionLayoutFake(text = "hello")
+        assertThat(selection.isCollapsed(layout)).isFalse()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_twoLayouts_empty_returnsTrue() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 0,
+            endSelectableId = 2L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = ""),
+                getSelectableInfoFake(selectableId = 2L, text = ""),
+            ),
+            startSlot = 1,
+            endSlot = 3,
+        )
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_twoLayouts_collapsed_returnsTrue() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 2L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 3,
+        )
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_twoLayouts_notCollapsedInFirst_returnsFalse() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 4,
+            endSelectableId = 2L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 3,
+        )
+        assertThat(selection.isCollapsed(layout)).isFalse()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_twoLayouts_notCollapsedInSecond_returnsFalse() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 2L,
+            endOffset = 1
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 3,
+        )
+        assertThat(selection.isCollapsed(layout)).isFalse()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_threeLayouts_empty_returnsTrue() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 0,
+            endSelectableId = 3L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = ""),
+                getSelectableInfoFake(selectableId = 2L, text = ""),
+                getSelectableInfoFake(selectableId = 3L, text = ""),
+            ),
+            startSlot = 1,
+            endSlot = 5,
+        )
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_threeLayouts_collapsed_returnsTrue() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 3L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = ""),
+                getSelectableInfoFake(selectableId = 3L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 5,
+        )
+        assertThat(selection.isCollapsed(layout)).isTrue()
+    }
+
+    @Test
+    fun selection_isCollapsed_layoutBuilder_threeLayouts_notCollapsed_returnsFalse() {
+        val selection = getSelection(
+            startSelectableId = 1L,
+            startOffset = 5,
+            endSelectableId = 3L,
+            endOffset = 0
+        )
+        val layout = getSelectionLayoutFake(
+            infos = listOf(
+                getSelectableInfoFake(selectableId = 1L, text = "hello"),
+                getSelectableInfoFake(selectableId = 2L, text = "."),
+                getSelectableInfoFake(selectableId = 3L, text = "hello"),
+            ),
+            startSlot = 1,
+            endSlot = 5,
+        )
+        assertThat(selection.isCollapsed(layout)).isFalse()
+    }
+
+    /** Calls [getTextFieldSelectionLayout] to get a [SelectionLayout]. */
+    @OptIn(ExperimentalContracts::class)
+    private fun buildSelectionLayoutForTest(
+        startHandlePosition: Offset = Offset(5f, 5f),
+        endHandlePosition: Offset = Offset(25f, 5f),
+        previousHandlePosition: Offset = Offset.Unspecified,
+        containerCoordinates: LayoutCoordinates = MockCoordinates(),
+        isStartHandle: Boolean = false,
+        previousSelection: Selection? = null,
+        selectableIdOrderingComparator: Comparator<Long> = naturalOrder(),
+        block: SelectionLayoutBuilder.() -> Unit,
+    ): SelectionLayout {
+        contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+        return SelectionLayoutBuilder(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = previousHandlePosition,
+            containerCoordinates = containerCoordinates,
+            isStartHandle = isStartHandle,
+            previousSelection = previousSelection,
+            selectableIdOrderingComparator = selectableIdOrderingComparator,
+        ).run {
+            block()
+            build()
+        }
+    }
+
+    private fun SelectionLayoutBuilder.appendInfoForTest(
+        selectableId: Long = 1L,
+        text: String = "hello",
+        rawStartHandleOffset: Int = 0,
+        startHandleDirection: Direction = Direction.ON,
+        rawEndHandleOffset: Int = 5,
+        endHandleDirection: Direction = Direction.ON,
+        rawPreviousHandleOffset: Int = -1,
+        rtlRanges: List<IntRange> = emptyList(),
+        wordBoundaries: List<TextRange> = listOf(),
+        lineBreaks: List<Int> = emptyList(),
+    ): SelectableInfo {
+        val layoutResult = getTextLayoutResultMock(
+            text = text,
+            rtlCharRanges = rtlRanges,
+            wordBoundaries = wordBoundaries,
+            lineBreaks = lineBreaks,
+        )
+        return appendInfo(
+            selectableId = selectableId,
+            rawStartHandleOffset = rawStartHandleOffset,
+            startHandleDirection = startHandleDirection,
+            rawEndHandleOffset = rawEndHandleOffset,
+            endHandleDirection = endHandleDirection,
+            rawPreviousHandleOffset = rawPreviousHandleOffset,
+            textLayoutResult = layoutResult
+        )
+    }
+
+    /** Calls [getTextFieldSelectionLayout] to get a [SelectionLayout]. */
+    private fun getSingleSelectionLayoutForTest(
+        text: String = "hello",
+        rawStartHandleOffset: Int = 0,
+        rawEndHandleOffset: Int = 5,
+        rawPreviousHandleOffset: Int = -1,
+        rtlRanges: List<IntRange> = emptyList(),
+        wordBoundaries: List<TextRange> = listOf(),
+        lineBreaks: List<Int> = emptyList(),
+        isStartHandle: Boolean = false,
+        previousSelection: Selection? = null,
+        isStartOfSelection: Boolean = previousSelection == null,
+    ): SelectionLayout {
+        val layoutResult = getTextLayoutResultMock(
+            text = text,
+            rtlCharRanges = rtlRanges,
+            wordBoundaries = wordBoundaries,
+            lineBreaks = lineBreaks,
+        )
+        return getTextFieldSelectionLayout(
+            layoutResult = layoutResult,
+            rawStartHandleOffset = rawStartHandleOffset,
+            rawEndHandleOffset = rawEndHandleOffset,
+            rawPreviousHandleOffset = rawPreviousHandleOffset,
+            previousSelectionRange = previousSelection?.toTextRange() ?: TextRange.Zero,
+            isStartOfSelection = isStartOfSelection,
+            isStartHandle = isStartHandle
+        )
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
deleted file mode 100644
index d33f417..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
+++ /dev/null
@@ -1,309 +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.compose.foundation.text.selection
-
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.style.ResolvedTextDirection
-import androidx.compose.ui.unit.IntSize
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.spy
-import org.mockito.kotlin.times
-import org.mockito.kotlin.verify
-
-@RunWith(JUnit4::class)
-class SelectionManagerDragTest {
-
-    private val selectionRegistrar = SelectionRegistrarImpl()
-    private val selectableKey = 1L
-    private val selectable = FakeSelectable().also { it.selectableId = this.selectableKey }
-    private val selectionManager = SelectionManager(selectionRegistrar)
-
-    private val size = IntSize(500, 600)
-    private val globalOffset = Offset(100f, 200f)
-    private val windowOffset = Offset(100f, 200f)
-    private val childToLocalOffset = Offset(300f, 400f)
-
-    private val containerLayoutCoordinates = spy(
-        MockCoordinates(
-            size = size,
-            globalOffset = globalOffset,
-            windowOffset = windowOffset,
-            childToLocalOffset = childToLocalOffset,
-            isAttached = true
-        )
-    )
-
-    private val startSelectable = FakeSelectable()
-    private val endSelectable = FakeSelectable()
-    private val startSelectableKey = 2L
-    private val endSelectableKey = 3L
-    private val startLayoutCoordinates = mock<LayoutCoordinates>()
-    private val endLayoutCoordinates = mock<LayoutCoordinates>()
-    private val fakeSubselection: Selection = Selection(
-        start = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 0,
-            selectableId = selectableKey
-        ),
-        end = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 5,
-            selectableId = selectableKey
-        )
-    )
-    private val fakeInitialSelection: Selection = Selection(
-        start = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 0,
-            selectableId = startSelectableKey
-        ),
-        end = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 5,
-            selectableId = endSelectableKey
-        )
-    )
-    private val fakeResultSelection: Selection = Selection(
-        start = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 5,
-            selectableId = endSelectableKey
-        ),
-        end = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 0,
-            selectableId = startSelectableKey
-        )
-    )
-    private var selection: Selection? = fakeInitialSelection
-    private val lambda: (Selection?) -> Unit = { selection = it }
-    private val spyLambda = spy(lambda)
-
-    @Before
-    fun setup() {
-        startSelectable.clear()
-        endSelectable.clear()
-        startSelectable.layoutCoordinate = startLayoutCoordinates
-        startSelectable.selectableId = startSelectableKey
-        endSelectable.layoutCoordinate = endLayoutCoordinates
-        endSelectable.selectableId = endSelectableKey
-
-        selectionRegistrar.subscribe(selectable)
-        selectionRegistrar.subscribe(startSelectable)
-        selectionRegistrar.subscribe(endSelectable)
-        selectionRegistrar.subselections = mapOf(
-            selectableKey to fakeSubselection
-        )
-
-        selectable.clear()
-        selectable.selectionToReturn = fakeResultSelection
-
-        selectionManager.containerLayoutCoordinates = containerLayoutCoordinates
-        selectionManager.onSelectionChange = spyLambda
-        selectionManager.selection = selection
-        selectionManager.hapticFeedBack = mock()
-    }
-
-    @Test
-    fun handleDragObserver_onStart_startHandle_enable_draggingHandle_get_startHandle_info() {
-        selectionManager.handleDragObserver(isStartHandle = true).onStart(Offset.Zero)
-
-        verify(containerLayoutCoordinates, times(1))
-            .localPositionOf(
-                sourceCoordinates = startLayoutCoordinates,
-                relativeToSource = getAdjustedCoordinates(Offset.Zero)
-            )
-        verify(spyLambda, times(0)).invoke(fakeResultSelection)
-    }
-
-    @Test
-    fun handleDragObserver_onStart_endHandle_enable_draggingHandle_get_endHandle_info() {
-        selectionManager.handleDragObserver(isStartHandle = false).onStart(Offset.Zero)
-
-        verify(containerLayoutCoordinates, times(1))
-            .localPositionOf(
-                sourceCoordinates = endLayoutCoordinates,
-                relativeToSource = getAdjustedCoordinates(Offset.Zero)
-            )
-        verify(spyLambda, times(0)).invoke(fakeResultSelection)
-    }
-
-    @Test
-    fun handleDragObserver_onDrag_startHandle_reuse_endHandle_calls_getSelection_change() {
-        val startOffset = Offset(30f, 50f)
-        val dragDistance = Offset(100f, 100f)
-        selectionManager.handleDragObserver(isStartHandle = true).onStart(startOffset)
-        selectionManager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
-
-        verify(containerLayoutCoordinates, times(1))
-            .localPositionOf(
-                sourceCoordinates = endLayoutCoordinates,
-                relativeToSource = getAdjustedCoordinates(Offset.Zero)
-            )
-
-        assertThat(selectable.getSelectionCalledTimes).isEqualTo(1)
-        assertThat(selectable.lastStartHandlePosition).isEqualTo(childToLocalOffset + dragDistance)
-        assertThat(selectable.lastEndHandlePosition).isEqualTo(childToLocalOffset)
-        assertThat(selectable.lastContainerLayoutCoordinates)
-            .isEqualTo(selectionManager.requireContainerCoordinates())
-        assertThat(selectable.lastAdjustment)
-            .isEqualTo(SelectionAdjustment.CharacterWithWordAccelerate)
-        assertThat(selectable.lastIsStartHandle).isEqualTo(true)
-        assertThat(selectable.lastPreviousSelection).isEqualTo(fakeSubselection)
-
-        assertThat(selection).isEqualTo(fakeResultSelection)
-        verify(spyLambda, times(1)).invoke(fakeResultSelection)
-    }
-
-    @Test
-    fun handleDragObserver_onDrag_endHandle_reuse_startHandle_calls_getSelection_change() {
-        val startOffset = Offset(30f, 50f)
-        val dragDistance = Offset(100f, 100f)
-        selectionManager.handleDragObserver(isStartHandle = false).onStart(startOffset)
-        selectionManager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
-
-        verify(containerLayoutCoordinates, times(1))
-            .localPositionOf(
-                sourceCoordinates = startLayoutCoordinates,
-                relativeToSource = getAdjustedCoordinates(Offset.Zero)
-            )
-
-        assertThat(selectable.getSelectionCalledTimes).isEqualTo(1)
-        assertThat(selectable.lastEndHandlePosition).isEqualTo(childToLocalOffset + dragDistance)
-        assertThat(selectable.lastStartHandlePosition).isEqualTo(childToLocalOffset)
-        assertThat(selectable.lastContainerLayoutCoordinates)
-            .isEqualTo(selectionManager.requireContainerCoordinates())
-        assertThat(selectable.lastAdjustment)
-            .isEqualTo(SelectionAdjustment.CharacterWithWordAccelerate)
-        assertThat(selectable.lastIsStartHandle).isEqualTo(false)
-        assertThat(selectable.lastPreviousSelection).isEqualTo(fakeSubselection)
-
-        assertThat(selection).isEqualTo(fakeResultSelection)
-        verify(spyLambda, times(1)).invoke(fakeResultSelection)
-    }
-
-    private fun getAdjustedCoordinates(position: Offset): Offset {
-        return Offset(position.x, position.y - 1f)
-    }
-}
-
-internal class FakeSelectable : Selectable {
-    override var selectableId = 0L
-    var lastEndHandlePosition: Offset? = null
-    var lastStartHandlePosition: Offset? = null
-    var lastPreviousHandlePosition: Offset? = null
-    var lastContainerLayoutCoordinates: LayoutCoordinates? = null
-    var lastAdjustment: SelectionAdjustment? = null
-    var lastPreviousSelection: Selection? = null
-    var lastIsStartHandle: Boolean? = null
-    var getSelectionCalledTimes = 0
-    var getTextCalledTimes = 0
-    var selectionToReturn: Selection? = null
-    var textToReturn: AnnotatedString? = null
-    var lastVisibleOffsetToReturn: Int = 0
-
-    var handlePosition = Offset.Zero
-    var boundingBox = Rect.Zero
-    var layoutCoordinate: LayoutCoordinates? = null
-
-    private val selectableKey = 1L
-    private val fakeSelectAllSelection: Selection = Selection(
-        start = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 0,
-            selectableId = selectableKey
-        ),
-        end = Selection.AnchorInfo(
-            direction = ResolvedTextDirection.Ltr,
-            offset = 10,
-            selectableId = selectableKey
-        )
-    )
-
-    override fun updateSelection(
-        startHandlePosition: Offset,
-        endHandlePosition: Offset,
-        previousHandlePosition: Offset?,
-        isStartHandle: Boolean,
-        containerLayoutCoordinates: LayoutCoordinates,
-        adjustment: SelectionAdjustment,
-        previousSelection: Selection?
-    ): Pair<Selection?, Boolean> {
-        getSelectionCalledTimes++
-        lastStartHandlePosition = startHandlePosition
-        lastEndHandlePosition = endHandlePosition
-        lastPreviousHandlePosition = previousHandlePosition
-        lastContainerLayoutCoordinates = containerLayoutCoordinates
-        lastAdjustment = adjustment
-        lastPreviousSelection = previousSelection
-        lastIsStartHandle = isStartHandle
-        return Pair(selectionToReturn, false)
-    }
-
-    override fun getSelectAllSelection(): Selection? {
-        return fakeSelectAllSelection
-    }
-
-    override fun getText(): AnnotatedString {
-        getTextCalledTimes++
-        return textToReturn!!
-    }
-
-    override fun getLayoutCoordinates(): LayoutCoordinates? {
-        return layoutCoordinate
-    }
-
-    override fun getHandlePosition(selection: Selection, isStartHandle: Boolean): Offset {
-        return handlePosition
-    }
-
-    override fun getBoundingBox(offset: Int): Rect {
-        return boundingBox
-    }
-
-    override fun getRangeOfLineContaining(offset: Int): TextRange {
-        return TextRange.Zero
-    }
-
-    override fun getLastVisibleOffset(): Int {
-        return lastVisibleOffsetToReturn
-    }
-
-    fun clear() {
-        lastEndHandlePosition = null
-        lastStartHandlePosition = null
-        lastPreviousHandlePosition = null
-        lastContainerLayoutCoordinates = null
-        lastAdjustment = null
-        lastPreviousSelection = null
-        lastIsStartHandle = null
-        getSelectionCalledTimes = 0
-        getTextCalledTimes = 0
-        selectionToReturn = null
-        textToReturn = null
-        lastVisibleOffsetToReturn = 0
-    }
-}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
index 7e6d97e..b72ff38 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
@@ -31,7 +31,6 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.mockito.kotlin.any
-import org.mockito.kotlin.argThat
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.isNull
@@ -48,6 +47,7 @@
     private val selectable = FakeSelectable()
     private val selectableId = 1L
     private val selectionManager = SelectionManager(selectionRegistrar)
+    private var onSelectionChangeCalledTimes = 0
 
     private val containerLayoutCoordinates = mock<LayoutCoordinates> {
         on { isAttached } doReturn true
@@ -73,9 +73,6 @@
         whenever(it.selectableId).thenReturn(lastSelectableId)
     }
 
-    private val startCoordinates = Offset(3f, 30f)
-    private val endCoordinates = Offset(3f, 600f)
-
     private val fakeSelection =
         Selection(
             start = Selection.AnchorInfo(
@@ -109,259 +106,120 @@
         selectionManager.clipboardManager = clipboardManager
         selectionManager.textToolbar = textToolbar
         selectionManager.selection = fakeSelection
+        selectionManager.onSelectionChange = { onSelectionChangeCalledTimes++ }
     }
 
     @Test
-    fun updateSelection_sorting() {
+    fun updateSelection_onInitial_returnsTrue() {
+        val startHandlePosition = Offset(x = 5f, y = 5f)
+        val endHandlePosition = Offset(x = 25f, y = 5f)
+        selectable.apply {
+            textToReturn = AnnotatedString("hello")
+            rawStartHandleOffset = 0
+            rawEndHandleOffset = 5
+        }
+
+        val actual = selectionManager.updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition - Offset(x = 5f, y = 0f),
+            isStartHandle = false,
+            adjustment = SelectionAdjustment.None,
+        )
+
+        assertThat(actual).isTrue()
+        assertThat(onSelectionChangeCalledTimes).isEqualTo(1)
+    }
+
+    @Test
+    fun updateSelection_onNoChange_returnsFalse() {
+        val startHandlePosition = Offset(x = 5f, y = 5f)
+        val endHandlePosition = Offset(x = 25f, y = 5f)
+        selectable.apply {
+            textToReturn = AnnotatedString("hello")
+            rawStartHandleOffset = 0
+            rawEndHandleOffset = 5
+            rawPreviousHandleOffset = 5
+        }
+
+        // run once to set context for the "previous" selection update
         selectionManager.updateSelection(
-            startHandlePosition = startCoordinates,
-            endHandlePosition = endCoordinates,
-            previousHandlePosition = null,
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition,
             isStartHandle = false,
-            adjustment = SelectionAdjustment.None
+            adjustment = SelectionAdjustment.None,
         )
 
-        verify(selectionRegistrar, times(1)).sort(containerLayoutCoordinates)
+        // run again since we are testing the "no changes" case
+        val actual = selectionManager.updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition,
+            isStartHandle = false,
+            adjustment = SelectionAdjustment.None,
+        )
+
+        assertThat(actual).isFalse()
+        assertThat(onSelectionChangeCalledTimes).isEqualTo(1)
     }
 
     @Test
-    fun updateSelection_single_selectable_calls_getSelection_once() {
-        val newSelection = fakeSelection.copy(
-            start = fakeSelection.start.copy(
-                offset = fakeSelection.start.offset + 1
-            )
-        )
+    fun updateSelection_onChange_returnsTrue() {
+        val startHandlePosition = Offset(x = 5f, y = 5f)
+        val endHandlePosition = Offset(x = 25f, y = 5f)
+        selectable.apply {
+            textToReturn = AnnotatedString("hello")
+            rawStartHandleOffset = 0
+            rawEndHandleOffset = 5
+            rawPreviousHandleOffset = 5
+        }
 
-        selectable.selectionToReturn = newSelection
-
+        // run once to set context for the "previous" selection update
         selectionManager.updateSelection(
-            startHandlePosition = startCoordinates,
-            endHandlePosition = endCoordinates,
-            previousHandlePosition = null,
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition,
             isStartHandle = false,
-            adjustment = SelectionAdjustment.None
+            adjustment = SelectionAdjustment.None,
         )
 
-        assertThat(selectable.getSelectionCalledTimes).isEqualTo(1)
-        assertThat(selectable.lastStartHandlePosition).isEqualTo(startCoordinates)
-        assertThat(selectable.lastEndHandlePosition).isEqualTo(endCoordinates)
-        assertThat(selectable.lastContainerLayoutCoordinates)
-            .isEqualTo(selectionManager.requireContainerCoordinates())
-        assertThat(selectable.lastAdjustment).isEqualTo(SelectionAdjustment.None)
-        assertThat(selectable.lastPreviousSelection).isEqualTo(fakeSelection)
+        // run again with a change in end handle
+        selectable.rawEndHandleOffset = 4
+        val actual = selectionManager.updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = endHandlePosition - Offset(x = 5f, y = 0f),
+            isStartHandle = false,
+            adjustment = SelectionAdjustment.None,
+        )
 
-        verify(
-            hapticFeedback,
-            times(1)
-        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+        assertThat(actual).isTrue()
+        assertThat(onSelectionChangeCalledTimes).isEqualTo(2)
     }
 
     @Test
-    fun updateSelection_multiple_selectables_calls_getSelection_multiple_times() {
-        val anotherSelectableId = 100L
-        val selectableAnother = mock<Selectable>()
-        whenever(selectableAnother.selectableId).thenReturn(anotherSelectableId)
-        whenever(
-            selectableAnother.updateSelection(
-                anyOffset(), anyOffset(), anyOffset(), any(), any(), any(), any()
-            )
-        ).thenReturn(Pair(null, false))
-        selectionRegistrar.subscribe(selectableAnother)
-        selectionRegistrar.subselections = mapOf(
-            anotherSelectableId to fakeSelection,
-            selectableId to fakeSelection
-        )
-
-        selectionManager.updateSelection(
-            startHandlePosition = startCoordinates,
-            endHandlePosition = endCoordinates,
-            previousHandlePosition = null,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None
-        )
-
-        assertThat(selectable.getSelectionCalledTimes).isEqualTo(1)
-        assertThat(selectable.lastStartHandlePosition).isEqualTo(startCoordinates)
-        assertThat(selectable.lastEndHandlePosition).isEqualTo(endCoordinates)
-        assertThat(selectable.lastContainerLayoutCoordinates)
-            .isEqualTo(selectionManager.requireContainerCoordinates())
-        assertThat(selectable.lastAdjustment).isEqualTo(SelectionAdjustment.None)
-        assertThat(selectable.lastPreviousSelection).isEqualTo(fakeSelection)
-
-        verify(selectableAnother, times(1))
-            .updateSelection(
-                startHandlePosition = startCoordinates,
-                endHandlePosition = endCoordinates,
-                previousHandlePosition = null,
-                isStartHandle = false,
-                containerLayoutCoordinates = selectionManager.requireContainerCoordinates(),
-                adjustment = SelectionAdjustment.None,
-                previousSelection = fakeSelection
-            )
-        verify(
-            hapticFeedback,
-            times(1)
-        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+    fun shouldPerformHaptics_notInTouchMode_returnsFalse() {
+        selectionManager.isInTouchMode = false
+        selectable.textToReturn = AnnotatedString("hello")
+        val actual = selectionManager.shouldPerformHaptics()
+        assertThat(actual).isFalse()
     }
 
     @Test
-    fun updateSelection_selection_does_not_change_hapticFeedBack_Not_triggered() {
-        val selection: Selection = fakeSelection
-        selectable.selectionToReturn = selection
-
-        selectionManager.updateSelection(
-            startHandlePosition = startCoordinates,
-            endHandlePosition = endCoordinates,
-            previousHandlePosition = null,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None
-        )
-
-        verify(
-            hapticFeedback,
-            times(0)
-        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+    fun shouldPerformHaptics_allEmptyTextSelectables_returnsFalse() {
+        selectionManager.isInTouchMode = true
+        selectable.textToReturn = AnnotatedString("")
+        val actual = selectionManager.shouldPerformHaptics()
+        assertThat(actual).isFalse()
     }
 
     @Test
-    fun updateSelection_selectable_drag_startHandle() {
-        selectionRegistrar.subscribe(endSelectable)
-        whenever(
-            endSelectable.updateSelection(
-                anyOffset(), anyOffset(), anyOffset(), any(), any(), any(), any()
-            )
-        ).thenReturn(Pair(null, false))
-        whenever(endSelectable.getLayoutCoordinates()).thenReturn(mock())
-        val previousStartHandlePosition = Offset(3f, 300f)
-        val newStartHandlePosition = Offset(3f, 600f)
-        selectionManager.updateSelection(
-            newPosition = newStartHandlePosition,
-            previousPosition = previousStartHandlePosition,
-            isStartHandle = true,
-            adjustment = SelectionAdjustment.None
-        )
-
-        assertThat(selectable.getSelectionCalledTimes).isEqualTo(1)
-        assertThat(selectable.lastStartHandlePosition).isEqualTo(newStartHandlePosition)
-        assertThat(selectable.lastPreviousHandlePosition).isEqualTo(previousStartHandlePosition)
-        assertThat(selectable.lastContainerLayoutCoordinates)
-            .isEqualTo(selectionManager.requireContainerCoordinates())
-        assertThat(selectable.lastAdjustment).isEqualTo(SelectionAdjustment.None)
-        assertThat(selectable.lastPreviousSelection).isEqualTo(fakeSelection)
-
-        verify(
-            hapticFeedback,
-            times(1)
-        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
-    }
-
-    @Test
-    fun updateSelection_selectable_drag_endHandle() {
-        selectionRegistrar.subscribe(startSelectable)
-        whenever(
-            startSelectable.updateSelection(
-                anyOffset(), anyOffset(), anyOffset(), any(), any(), any(), any()
-            )
-        ).thenReturn(Pair(null, false))
-        whenever(startSelectable.getLayoutCoordinates()).thenReturn(mock())
-        val previousStartHandlePosition = Offset(3f, 300f)
-        val newStartHandlePosition = Offset(3f, 600f)
-        selectionManager.updateSelection(
-            newPosition = newStartHandlePosition,
-            previousPosition = previousStartHandlePosition,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None
-        )
-
-        assertThat(selectable.getSelectionCalledTimes).isEqualTo(1)
-        assertThat(selectable.lastEndHandlePosition).isEqualTo(newStartHandlePosition)
-        assertThat(selectable.lastPreviousHandlePosition).isEqualTo(previousStartHandlePosition)
-        assertThat(selectable.lastContainerLayoutCoordinates)
-            .isEqualTo(selectionManager.requireContainerCoordinates())
-        assertThat(selectable.lastAdjustment).isEqualTo(SelectionAdjustment.None)
-        assertThat(selectable.lastPreviousSelection).isEqualTo(fakeSelection)
-
-        verify(
-            hapticFeedback,
-            times(1)
-        ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
-    }
-
-    @Test
-    fun updateSelection_consumeDrag_return_true() {
-        selectionRegistrar.subscribe(startSelectable)
-        // The start selectable returns true and consumes the drag.
-        whenever(
-            startSelectable.updateSelection(
-                anyOffset(), anyOffset(), anyOffset(), any(), any(), any(), any()
-            )
-        ).thenReturn(Pair(null, true))
-        whenever(startSelectable.getLayoutCoordinates()).thenReturn(mock())
-
-        val previousStartHandlePosition = Offset(3f, 300f)
-        val newStartHandlePosition = Offset(3f, 600f)
-
-        val consumed = selectionManager.updateSelection(
-            newPosition = newStartHandlePosition,
-            previousPosition = previousStartHandlePosition,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None
-        )
-
-        assertThat(consumed).isTrue()
-    }
-
-    @Test
-    fun updateSelection_notConsumeDrag_return_false() {
-        selectionRegistrar.subscribe(startSelectable)
-        // The start selectable returns false and does not consume the drag.
-        whenever(
-            startSelectable.updateSelection(
-                anyOffset(), anyOffset(), anyOffset(), any(), any(), any(), any()
-            )
-        ).thenReturn(Pair(null, false))
-        whenever(startSelectable.getLayoutCoordinates()).thenReturn(mock())
-
-        // selection cannot change, else consumed will be true.
-        // the updated selection is null, so set the initial selection to null as well.
-        selectionManager.selection = null
-
-        val previousStartHandlePosition = Offset(3f, 300f)
-        val newStartHandlePosition = Offset(3f, 600f)
-
-        val consumed = selectionManager.updateSelection(
-            newPosition = newStartHandlePosition,
-            previousPosition = previousStartHandlePosition,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None
-        )
-
-        assertThat(consumed).isFalse()
-    }
-
-    @Test
-    fun updateSelection_notConsumeDrag_butSelectionChange_return_true() {
-        selectionRegistrar.subscribe(startSelectable)
-        // The start selectable returns false and does not consume the drag.
-        whenever(
-            startSelectable.updateSelection(
-                anyOffset(), anyOffset(), anyOffset(), any(), any(), any(), any()
-            )
-        ).thenReturn(Pair(null, false))
-        whenever(startSelectable.getLayoutCoordinates()).thenReturn(mock())
-
-        val previousStartHandlePosition = Offset(3f, 300f)
-        val newStartHandlePosition = Offset(3f, 600f)
-
-        // new selection is null, so it will be counted as a change
-        val consumed = selectionManager.updateSelection(
-            newPosition = newStartHandlePosition,
-            previousPosition = previousStartHandlePosition,
-            isStartHandle = false,
-            adjustment = SelectionAdjustment.None
-        )
-
-        assertThat(consumed).isTrue()
+    fun shouldPerformHaptics_inTouchModeAndNonEmpty_returnsTrue() {
+        selectionManager.isInTouchMode = true
+        selectable.textToReturn = AnnotatedString("hello")
+        val actual = selectionManager.shouldPerformHaptics()
+        assertThat(actual).isTrue()
     }
 
     @Test
@@ -666,7 +524,7 @@
     }
 
     @Test
-    fun copy_selection_null_not_trigger_clipboardmanager() {
+    fun copy_selection_null_not_trigger_clipboardManager() {
         selectionManager.selection = null
 
         selectionManager.copy()
@@ -675,7 +533,7 @@
     }
 
     @Test
-    fun copy_selection_not_null_trigger_clipboardmanager_setText() {
+    fun copy_selection_not_null_trigger_clipboardManager_setText() {
         val text = "Text Demo"
         val annotatedString = AnnotatedString(text = text)
         val startOffset = text.indexOf('m')
@@ -839,9 +697,3 @@
         ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
     }
 }
-
-private fun anyOffset(): Offset {
-    return argThat { any: Any? ->
-        any == null || any is Long || any is Offset
-    } as Offset? ?: Offset.Zero
-}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
index 65bd513..2152280 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
@@ -32,7 +32,6 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-@Ignore("b/297398943")
 class RecyclerViewAsCarouselBenchmark(
     private val compilationMode: CompilationMode
 ) {
@@ -47,6 +46,7 @@
     }
 
     @Test
+    @Ignore("b/297398943")
     fun scroll() {
         val carousel = device.findObject(
             By.res(
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/RangeSliderBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/RangeSliderBenchmark.kt
index 270c5dd..ae7504d 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/RangeSliderBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/RangeSliderBenchmark.kt
@@ -29,6 +29,7 @@
 import androidx.compose.testutils.LayeredComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
@@ -45,7 +46,7 @@
 
     @Test
     fun firstPixel() {
-        benchmarkRule.benchmarkFirstRenderUntilStable(sliderTestCaseFactory)
+        benchmarkRule.benchmarkToFirstPixel(sliderTestCaseFactory)
     }
 
     @Test
@@ -87,7 +88,7 @@
 
     override fun toggleState() {
         if (state.activeRangeStart == 0f) {
-            state.activeRangeStart = 1f
+            state.activeRangeStart = .7f
         } else {
             state.activeRangeStart = 0f
         }
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 9236b0f..d665b1b 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1206,22 +1206,22 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SegmentedButtonDefaults {
     method @androidx.compose.runtime.Composable public void ActiveIcon();
-    method @androidx.compose.runtime.Composable public void SegmentedButtonIcon(boolean active, optional kotlin.jvm.functions.Function0<kotlin.Unit> activeContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? inactiveContent);
+    method @androidx.compose.runtime.Composable public void Icon(boolean active, optional kotlin.jvm.functions.Function0<kotlin.Unit> activeContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? inactiveContent);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SegmentedButtonColors colors(optional long activeContainerColor, optional long activeContentColor, optional long activeBorderColor, optional long inactiveContainerColor, optional long inactiveContentColor, optional long inactiveBorderColor, optional long disabledActiveContainerColor, optional long disabledActiveContentColor, optional long disabledActiveBorderColor, optional long disabledInactiveContainerColor, optional long disabledInactiveContentColor, optional long disabledInactiveBorderColor);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.foundation.shape.CornerBasedShape getBaseShape();
     method public androidx.compose.material3.SegmentedButtonBorder getBorder();
     method public float getIconSize();
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.foundation.shape.CornerBasedShape getShape();
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape shape(int position, int count, optional androidx.compose.foundation.shape.CornerBasedShape shape);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape itemShape(int index, int count, optional androidx.compose.foundation.shape.CornerBasedShape baseShape);
     property public final androidx.compose.material3.SegmentedButtonBorder Border;
     property public final float IconSize;
-    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.foundation.shape.CornerBasedShape Shape;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.foundation.shape.CornerBasedShape baseShape;
     field public static final androidx.compose.material3.SegmentedButtonDefaults INSTANCE;
   }
 
   public final class SegmentedButtonKt {
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MultiChoiceSegmentedButtonRow(optional androidx.compose.ui.Modifier modifier, optional float space, kotlin.jvm.functions.Function1<? super androidx.compose.material3.MultiChoiceSegmentedButtonRowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.MultiChoiceSegmentedButtonRowScope, boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.SingleChoiceSegmentedButtonRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.MultiChoiceSegmentedButtonRowScope, boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> label);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.SingleChoiceSegmentedButtonRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> label);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SingleChoiceSegmentedButtonRow(optional androidx.compose.ui.Modifier modifier, optional float space, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SingleChoiceSegmentedButtonRowScope,kotlin.Unit> content);
   }
 
@@ -1348,7 +1348,7 @@
   @androidx.compose.runtime.Stable public final class SliderDefaults {
     method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
     method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
-    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
+    method @Deprecated @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
@@ -1363,16 +1363,18 @@
     method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
-  @androidx.compose.runtime.Stable public final class SliderPositions {
-    ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
-    method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
-    method public float[] getTickFractions();
-    property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
-    property public final float[] tickFractions;
+  @Deprecated @androidx.compose.runtime.Stable public final class SliderPositions {
+    ctor @Deprecated public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
+    method @Deprecated public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
+    method @Deprecated public float[] getTickFractions();
+    property @Deprecated public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
+    property @Deprecated public final float[] tickFractions;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState implements androidx.compose.foundation.gestures.DraggableState {
     ctor public SliderState(optional float initialValue, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>? initialOnValueChange, optional @IntRange(from=0L) int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
+    method public void dispatchRawDelta(float delta);
+    method public suspend Object? drag(androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnValueChangeFinished();
     method public int getSteps();
     method public float getValue();
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 9236b0f..d665b1b 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1206,22 +1206,22 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SegmentedButtonDefaults {
     method @androidx.compose.runtime.Composable public void ActiveIcon();
-    method @androidx.compose.runtime.Composable public void SegmentedButtonIcon(boolean active, optional kotlin.jvm.functions.Function0<kotlin.Unit> activeContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? inactiveContent);
+    method @androidx.compose.runtime.Composable public void Icon(boolean active, optional kotlin.jvm.functions.Function0<kotlin.Unit> activeContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? inactiveContent);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SegmentedButtonColors colors(optional long activeContainerColor, optional long activeContentColor, optional long activeBorderColor, optional long inactiveContainerColor, optional long inactiveContentColor, optional long inactiveBorderColor, optional long disabledActiveContainerColor, optional long disabledActiveContentColor, optional long disabledActiveBorderColor, optional long disabledInactiveContainerColor, optional long disabledInactiveContentColor, optional long disabledInactiveBorderColor);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.foundation.shape.CornerBasedShape getBaseShape();
     method public androidx.compose.material3.SegmentedButtonBorder getBorder();
     method public float getIconSize();
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.foundation.shape.CornerBasedShape getShape();
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape shape(int position, int count, optional androidx.compose.foundation.shape.CornerBasedShape shape);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape itemShape(int index, int count, optional androidx.compose.foundation.shape.CornerBasedShape baseShape);
     property public final androidx.compose.material3.SegmentedButtonBorder Border;
     property public final float IconSize;
-    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.foundation.shape.CornerBasedShape Shape;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.foundation.shape.CornerBasedShape baseShape;
     field public static final androidx.compose.material3.SegmentedButtonDefaults INSTANCE;
   }
 
   public final class SegmentedButtonKt {
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MultiChoiceSegmentedButtonRow(optional androidx.compose.ui.Modifier modifier, optional float space, kotlin.jvm.functions.Function1<? super androidx.compose.material3.MultiChoiceSegmentedButtonRowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.MultiChoiceSegmentedButtonRowScope, boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.SingleChoiceSegmentedButtonRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.MultiChoiceSegmentedButtonRowScope, boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> label);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SegmentedButton(androidx.compose.material3.SingleChoiceSegmentedButtonRowScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SegmentedButtonColors colors, optional androidx.compose.material3.SegmentedButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit> label);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SingleChoiceSegmentedButtonRow(optional androidx.compose.ui.Modifier modifier, optional float space, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SingleChoiceSegmentedButtonRowScope,kotlin.Unit> content);
   }
 
@@ -1348,7 +1348,7 @@
   @androidx.compose.runtime.Stable public final class SliderDefaults {
     method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
     method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
-    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
+    method @Deprecated @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
@@ -1363,16 +1363,18 @@
     method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
-  @androidx.compose.runtime.Stable public final class SliderPositions {
-    ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
-    method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
-    method public float[] getTickFractions();
-    property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
-    property public final float[] tickFractions;
+  @Deprecated @androidx.compose.runtime.Stable public final class SliderPositions {
+    ctor @Deprecated public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
+    method @Deprecated public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
+    method @Deprecated public float[] getTickFractions();
+    property @Deprecated public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
+    property @Deprecated public final float[] tickFractions;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState implements androidx.compose.foundation.gestures.DraggableState {
     ctor public SliderState(optional float initialValue, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>? initialOnValueChange, optional @IntRange(from=0L) int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
+    method public void dispatchRawDelta(float delta);
+    method public suspend Object? drag(androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnValueChangeFinished();
     method public int getSteps();
     method public float getValue();
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
index ba51a05..eb57d09 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
@@ -48,7 +48,7 @@
     SingleChoiceSegmentedButtonRow {
         options.forEachIndexed { index, label ->
             SegmentedButton(
-                shape = SegmentedButtonDefaults.shape(position = index, count = options.size),
+                shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
                 onClick = { selectedIndex = index },
                 selected = index == selectedIndex
             ) {
@@ -73,9 +73,9 @@
     MultiChoiceSegmentedButtonRow {
         options.forEachIndexed { index, label ->
             SegmentedButton(
-                shape = SegmentedButtonDefaults.shape(position = index, count = options.size),
+                shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
                 icon = {
-                    SegmentedButtonDefaults.SegmentedButtonIcon(active = index in checkedList) {
+                    SegmentedButtonDefaults.Icon(active = index in checkedList) {
                         Icon(
                             imageVector = icons[index],
                             contentDescription = null,
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt
index 89ae9dd..e079c56 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonScreenshotTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -53,7 +54,11 @@
         rule.setMaterialContent(lightColorScheme()) {
             MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
                 values.forEach {
-                    SegmentedButton(checked = false, onCheckedChange = {}) {
+                    SegmentedButton(
+                        checked = false,
+                        onCheckedChange = {},
+                        shape = RectangleShape,
+                    ) {
                         Text(it)
                     }
                 }
@@ -68,7 +73,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
                 values.forEach {
-                    SegmentedButton(checked = true, onCheckedChange = {}) {
+                    SegmentedButton(checked = true, onCheckedChange = {}, shape = RectangleShape) {
                         Text(it)
                     }
                 }
@@ -83,7 +88,11 @@
         rule.setMaterialContent(lightColorScheme()) {
             MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
                 values.forEachIndexed { index, item ->
-                    SegmentedButton(checked = index == 1, onCheckedChange = {}) {
+                    SegmentedButton(
+                        checked = index == 1,
+                        onCheckedChange = {},
+                        shape = RectangleShape,
+                    ) {
                         Text(item)
                     }
                 }
@@ -111,7 +120,8 @@
                                     modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
                                 )
                             }
-                        }
+                        },
+                        shape = RectangleShape,
                     ) {
                         Text(item)
                     }
@@ -134,7 +144,8 @@
                     SegmentedButton(
                         checked = index == 1,
                         onCheckedChange = {},
-                        colors = colors
+                        colors = colors,
+                        shape = RectangleShape,
                     ) {
                         Text(item)
                     }
@@ -154,7 +165,7 @@
             )
             MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
                 values.forEachIndexed { index, item ->
-                    val shape = SegmentedButtonDefaults.shape(index, values.size)
+                    val shape = SegmentedButtonDefaults.itemShape(index, values.size)
 
                     SegmentedButton(
                         checked = index == 1,
@@ -176,7 +187,11 @@
         rule.setMaterialContent(darkColorScheme()) {
             MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
                 values.forEach {
-                    SegmentedButton(checked = false, onCheckedChange = {}) {
+                    SegmentedButton(
+                        checked = false,
+                        onCheckedChange = {},
+                        shape = RectangleShape,
+                    ) {
                         Text(it)
                     }
                 }
@@ -191,7 +206,7 @@
         rule.setMaterialContent(darkColorScheme()) {
             MultiChoiceSegmentedButtonRow(modifier = Modifier.testTag(testTag)) {
                 values.forEach {
-                    SegmentedButton(checked = true, onCheckedChange = {}) {
+                    SegmentedButton(checked = true, onCheckedChange = {}, shape = RectangleShape) {
                         Text(it)
                     }
                 }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt
index 37c5862..8f1a178 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SegmentedButtonTest.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -59,7 +60,11 @@
         rule.setMaterialContent(lightColorScheme()) {
             MultiChoiceSegmentedButtonRow {
                 values.forEach {
-                    SegmentedButton(checked = false, onCheckedChange = {}) {
+                    SegmentedButton(
+                        shape = RectangleShape,
+                        checked = false,
+                        onCheckedChange = {}
+                    ) {
                         Text(it)
                     }
                 }
@@ -76,7 +81,11 @@
         rule.setMaterialContent(lightColorScheme()) {
             SingleChoiceSegmentedButtonRow {
                 values.forEach {
-                    SegmentedButton(selected = false, onClick = {}) {
+                    SegmentedButton(
+                        shape = RectangleShape,
+                        selected = false,
+                        onClick = {}
+                    ) {
                         Text(it)
                     }
                 }
@@ -91,10 +100,18 @@
         var checked by mutableStateOf(true)
         rule.setMaterialContent(lightColorScheme()) {
             MultiChoiceSegmentedButtonRow {
-                SegmentedButton(onCheckedChange = { checked = it }, checked = checked) {
+                SegmentedButton(
+                    onCheckedChange = { checked = it },
+                    checked = checked,
+                    shape = RectangleShape,
+                ) {
                     Text("Day")
                 }
-                SegmentedButton(onCheckedChange = { checked = it }, checked = !checked) {
+                SegmentedButton(
+                    onCheckedChange = { checked = it },
+                    checked = !checked,
+                    shape = RectangleShape,
+                ) {
                     Text("Month")
                 }
             }
@@ -115,10 +132,18 @@
     fun selectableSegmentedButton_semantics() {
         rule.setMaterialContent(lightColorScheme()) {
             SingleChoiceSegmentedButtonRow(modifier = Modifier.testTag("row")) {
-                SegmentedButton(selected = false, onClick = {}) {
+                SegmentedButton(
+                    selected = false,
+                    onClick = {},
+                    shape = RectangleShape,
+                ) {
                     Text("Day")
                 }
-                SegmentedButton(selected = false, onClick = {}) {
+                SegmentedButton(
+                    selected = false,
+                    onClick = {},
+                    shape = RectangleShape,
+                ) {
                     Text("Month")
                 }
             }
@@ -139,6 +164,7 @@
                     checked = checked,
                     onCheckedChange = {},
                     icon = { Text(if (checked) "checked" else "unchecked") },
+                    shape = RectangleShape,
                 ) {
                     Text("Day")
                 }
@@ -161,10 +187,10 @@
             parentMaxWidth = 300.dp, parentMaxHeight = 100.dp
         ) {
             MultiChoiceSegmentedButtonRow {
-                SegmentedButton(checked = false, onCheckedChange = {}) {
+                SegmentedButton(checked = false, onCheckedChange = {}, shape = RectangleShape) {
                     Text(modifier = Modifier.width(60.dp), text = "Day")
                 }
-                SegmentedButton(checked = false, onCheckedChange = {}) {
+                SegmentedButton(checked = false, onCheckedChange = {}, shape = RectangleShape) {
                     Text(modifier = Modifier.width(30.dp), text = "Month")
                 }
             }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
index 8696ba5..a06c97d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
@@ -481,7 +481,9 @@
                 Spacer(Modifier.width(spacerWidth))
                 Slider(
                     state = SliderState(0f, {}),
-                    modifier = Modifier.testTag(tag).weight(1f)
+                    modifier = Modifier
+                        .testTag(tag)
+                        .weight(1f)
                 )
                 Spacer(Modifier.width(spacerWidth))
             }
@@ -1287,7 +1289,7 @@
 
         rule.runOnIdle {
             Truth.assertThat(recompositionCounter.outerRecomposition).isEqualTo(1)
-            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(3)
+            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(4)
         }
     }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SegmentedButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SegmentedButton.kt
index 860b1a8..9f2c7b0 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SegmentedButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SegmentedButton.kt
@@ -32,6 +32,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
@@ -61,13 +62,9 @@
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -75,7 +72,14 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.MultiContentMeasurePolicy
 import androidx.compose.ui.layout.layout
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -83,6 +87,7 @@
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.util.fastMaxBy
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
 /**
@@ -103,11 +108,11 @@
  * @param checked whether this button is checked or not
  * @param onCheckedChange callback to be invoked when the button is clicked.
  * therefore the change of checked state in requested.
+ * @param shape the shape for this button
  * @param modifier the [Modifier] to be applied to this button
  * @param enabled controls the enabled state of this button. When `false`, this component will not
  * respond to user input, and it will appear visually disabled and disabled to accessibility
  * services.
- * @param shape the shape for this button
  * @param colors [SegmentedButtonColors] that will be used to resolve the colors used for this
  * @param border the border for this button, see [SegmentedButtonColors]
  * Button in different states
@@ -117,32 +122,30 @@
  * @param icon the icon slot for this button, you can pass null in unchecked, in which case
  * the content will displace to show the checked icon, or pass different icon lambdas for
  * unchecked and checked in which case the icons will crossfade.
- * @param content content to be rendered inside this button
+ * @param label content to be rendered inside this button
  */
 @Composable
 @ExperimentalMaterial3Api
 fun MultiChoiceSegmentedButtonRowScope.SegmentedButton(
     checked: Boolean,
     onCheckedChange: (Boolean) -> Unit,
+    shape: Shape,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shape: Shape = RectangleShape,
     colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(),
     border: SegmentedButtonBorder = SegmentedButtonDefaults.Border,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    icon: @Composable () -> Unit = { SegmentedButtonDefaults.SegmentedButtonIcon(checked) },
-    content: @Composable () -> Unit,
+    icon: @Composable () -> Unit = { SegmentedButtonDefaults.Icon(checked) },
+    label: @Composable () -> Unit,
 ) {
-
     val containerColor = colors.containerColor(enabled, checked)
     val contentColor = colors.contentColor(enabled, checked)
-    val checkedState by rememberUpdatedState(checked)
-    val interactionCount by interactionSource.interactionCountAsState()
+    val interactionCount = interactionSource.interactionCountAsState()
 
     Surface(
         modifier = modifier
             .weight(1f)
-            .interactionZIndex(checkedState, interactionCount)
+            .interactionZIndex(checked, interactionCount)
             .defaultMinSize(
                 minWidth = ButtonDefaults.MinWidth,
                 minHeight = ButtonDefaults.MinHeight
@@ -156,7 +159,7 @@
         border = border.borderStroke(enabled, checked, colors),
         interactionSource = interactionSource
     ) {
-        SegmentedButtonContent(icon, content)
+        SegmentedButtonContent(icon, label)
     }
 }
 
@@ -178,11 +181,11 @@
  * @param selected whether this button is selected or not
  * @param onClick callback to be invoked when the button is clicked.
  * therefore the change of checked state in requested.
+ * @param shape the shape for this button
  * @param modifier the [Modifier] to be applied to this button
  * @param enabled controls the enabled state of this button. When `false`, this component will not
  * respond to user input, and it will appear visually disabled and disabled to accessibility
  * services.
- * @param shape the shape for this button
  * @param colors [SegmentedButtonColors] that will be used to resolve the colors used for this
  * @param border the border for this button, see [SegmentedButtonColors]
  * Button in different states
@@ -192,35 +195,34 @@
  * @param icon the icon slot for this button, you can pass null in unchecked, in which case
  * the content will displace to show the checked icon, or pass different icon lambdas for
  * unchecked and checked in which case the icons will crossfade.
- * @param content content to be rendered inside this button
+ * @param label content to be rendered inside this button
  */
 @Composable
 @ExperimentalMaterial3Api
 fun SingleChoiceSegmentedButtonRowScope.SegmentedButton(
     selected: Boolean,
     onClick: () -> Unit,
+    shape: Shape,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shape: Shape = RectangleShape,
     colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(),
     border: SegmentedButtonBorder = SegmentedButtonDefaults.Border,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    icon: @Composable () -> Unit = { SegmentedButtonDefaults.SegmentedButtonIcon(selected) },
-    content: @Composable () -> Unit,
+    icon: @Composable () -> Unit = { SegmentedButtonDefaults.Icon(selected) },
+    label: @Composable () -> Unit,
 ) {
     val containerColor = colors.containerColor(enabled, selected)
     val contentColor = colors.contentColor(enabled, selected)
-    val checkedState by rememberUpdatedState(selected)
-    val interactionCount by interactionSource.interactionCountAsState()
+    val interactionCount = interactionSource.interactionCountAsState()
 
     Surface(
         modifier = modifier
             .weight(1f)
-            .interactionZIndex(checkedState, interactionCount)
+            .interactionZIndex(selected, interactionCount)
             .defaultMinSize(
                 minWidth = ButtonDefaults.MinWidth,
                 minHeight = ButtonDefaults.MinHeight
-            ),
+            ).semantics { role = Role.RadioButton },
         selected = selected,
         onClick = onClick,
         enabled = enabled,
@@ -230,7 +232,7 @@
         border = border.borderStroke(enabled, selected, colors),
         interactionSource = interactionSource
     ) {
-        SegmentedButtonContent(icon, content)
+        SegmentedButtonContent(icon, label)
     }
 }
 
@@ -313,57 +315,76 @@
     icon: @Composable () -> Unit,
     content: @Composable () -> Unit,
 ) {
-    Row(
-        modifier = Modifier.padding(ButtonDefaults.TextButtonContentPadding),
-        horizontalArrangement = Arrangement.Center,
-        verticalAlignment = Alignment.CenterVertically
-    ) {
-        ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
-            var animatable by remember {
-                mutableStateOf<Animatable<Int, AnimationVector1D>?>(null)
+    Box(contentAlignment = Alignment.Center) {
+        val typography =
+            MaterialTheme.typography.fromToken(OutlinedSegmentedButtonTokens.LabelTextFont)
+        ProvideTextStyle(typography) {
+            val scope = rememberCoroutineScope()
+            val measurePolicy = remember { SegmentedButtonContentMeasurePolicy(scope) }
+
+            Layout(
+                modifier = Modifier.padding(ButtonDefaults.TextButtonContentPadding),
+                contents = listOf(icon, content),
+                measurePolicy = measurePolicy
+            )
+        }
+    }
+}
+
+internal class SegmentedButtonContentMeasurePolicy(
+    val scope: CoroutineScope
+) : MultiContentMeasurePolicy {
+    var animatable: Animatable<Int, AnimationVector1D>? = null
+    private var initialOffset: Int? = null
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    override fun MeasureScope.measure(
+        measurables: List<List<Measurable>>,
+        constraints: Constraints
+    ): MeasureResult {
+        val (iconMeasurables, contentMeasurables) = measurables
+        val iconPlaceables = iconMeasurables.fastMap { it.measure(constraints) }
+        val iconDesiredWidth = iconMeasurables.fastFold(0) { acc, it ->
+            maxOf(acc, it.maxIntrinsicWidth(Constraints.Infinity))
+        }
+        val iconWidth = iconPlaceables.fastMaxBy { it.width }?.width ?: 0
+        val contentPlaceables = contentMeasurables.fastMap { it.measure(constraints) }
+        val contentWidth = contentPlaceables.fastMaxBy { it.width }?.width
+        val width = maxOf(SegmentedButtonDefaults.IconSize.roundToPx(), iconDesiredWidth) +
+            IconSpacing.roundToPx() +
+            (contentWidth ?: 0)
+
+        val offsetX = if (iconWidth == 0) {
+            -(SegmentedButtonDefaults.IconSize.roundToPx() + IconSpacing.roundToPx()) / 2
+        } else {
+            iconDesiredWidth - SegmentedButtonDefaults.IconSize.roundToPx()
+        }
+
+        if (initialOffset == null) {
+            initialOffset = offsetX
+        } else {
+            val anim = animatable ?: Animatable(initialOffset!!, Int.VectorConverter)
+                .also { animatable = it }
+            if (anim.targetValue != offsetX) {
+                scope.launch {
+                    anim.animateTo(offsetX, tween(MotionTokens.DurationMedium3.toInt()))
+                }
+            }
+        }
+
+        return layout(width, constraints.maxHeight) {
+            iconPlaceables.fastForEach {
+                it.place(0, (constraints.maxHeight - it.height) / 2)
             }
 
-            val scope = rememberCoroutineScope()
+            val contentOffsetX = SegmentedButtonDefaults.IconSize.roundToPx() +
+                IconSpacing.roundToPx() + (animatable?.value ?: offsetX)
 
-            Layout(listOf(icon, content)) { (iconMeasurables, contentMeasurables), constraints ->
-                val iconPlaceables = iconMeasurables.fastMap { it.measure(constraints) }
-                val iconDesiredWidth = iconMeasurables.fastFold(0) { acc, it ->
-                    maxOf(acc, it.maxIntrinsicWidth(Constraints.Infinity))
-                }
-                val iconWidth = iconPlaceables.fastMaxBy { it.width }?.width ?: 0
-                val contentPlaceables = contentMeasurables.fastMap { it.measure(constraints) }
-                val contentWidth = contentPlaceables.fastMaxBy { it.width }?.width
-                val width = maxOf(SegmentedButtonDefaults.IconSize.roundToPx(), iconDesiredWidth) +
-                    IconSpacing.roundToPx() +
-                    (contentWidth ?: 0)
-
-                val offsetX = if (iconWidth == 0) {
-                    -(SegmentedButtonDefaults.IconSize.roundToPx() + IconSpacing.roundToPx()) / 2
-                } else {
-                    iconDesiredWidth - SegmentedButtonDefaults.IconSize.roundToPx()
-                }
-
-                val anim = animatable ?: Animatable(offsetX, Int.VectorConverter)
-                    .also { animatable = it }
-
-                if (anim.targetValue != offsetX) {
-                    scope.launch {
-                        anim.animateTo(offsetX, tween(MotionTokens.DurationMedium3.toInt()))
-                    }
-                }
-
-                layout(width, constraints.maxHeight) {
-                    iconPlaceables.fastForEach {
-                        it.place(0, (constraints.maxHeight - it.height) / 2)
-                    }
-
-                    val contentOffsetX = SegmentedButtonDefaults.IconSize.roundToPx() +
-                        IconSpacing.roundToPx() + anim.value
-
-                    contentPlaceables.fastForEach {
-                        it.place(contentOffsetX, (constraints.maxHeight - it.height) / 2)
-                    }
-                }
+            contentPlaceables.fastForEach {
+                it.place(
+                    contentOffsetX,
+                    (constraints.maxHeight - it.height) / 2
+                )
             }
         }
     }
@@ -458,26 +479,34 @@
     /** The default [BorderStroke] factory used by [SegmentedButton]. */
     val Border = SegmentedButtonBorder(width = OutlinedSegmentedButtonTokens.OutlineWidth)
 
-    /** The default [Shape] for [SegmentedButton]. */
-    val Shape: CornerBasedShape
+    /**
+     * The shape of the segmented button container, for correct behavior this should or the desired
+     * [CornerBasedShape] should be used with [itemShape] and passed to each segmented button.
+     */
+    val baseShape: CornerBasedShape
         @Composable
         @ReadOnlyComposable
         get() = OutlinedSegmentedButtonTokens.Shape.value as CornerBasedShape
 
     /**
-     * A shape constructor that the button in [position] should have when there are [count] buttons
+     * A shape constructor that the button in [index] should have when there are [count] buttons in
+     * the container.
      *
-     * @param position the position for this button in the row
+     * @param index the index for this button in the row
      * @param count the count of buttons in this row
-     * @param shape the [CornerBasedShape] the base shape that should be used in buttons that are
-     * not in the start or the end.
+     * @param baseShape the [CornerBasedShape] the base shape that should be used in buttons that
+     * are not in the start or the end.
      */
     @Composable
     @ReadOnlyComposable
-    fun shape(position: Int, count: Int, shape: CornerBasedShape = this.Shape): Shape {
-        return when (position) {
-            0 -> shape.start()
-            count - 1 -> shape.end()
+    fun itemShape(index: Int, count: Int, baseShape: CornerBasedShape = this.baseShape): Shape {
+        if (count == 1) {
+            return baseShape
+        }
+
+        return when (index) {
+            0 -> baseShape.start()
+            count - 1 -> baseShape.end()
             else -> RectangleShape
         }
     }
@@ -506,7 +535,7 @@
      * checked.
      */
     @Composable
-    fun SegmentedButtonIcon(
+    fun Icon(
         active: Boolean,
         activeContent: @Composable () -> Unit = { ActiveIcon() },
         inactiveContent: (@Composable () -> Unit)? = null
@@ -676,11 +705,11 @@
     }
 }
 
-private fun Modifier.interactionZIndex(checked: Boolean, interactionCount: Int) =
+private fun Modifier.interactionZIndex(checked: Boolean, interactionCount: State<Int>) =
     this.layout { measurable, constraints ->
         val placeable = measurable.measure(constraints)
         layout(placeable.width, placeable.height) {
-            val zIndex = interactionCount + if (checked) CheckedZIndexFactor else 0f
+            val zIndex = interactionCount.value + if (checked) CheckedZIndexFactor else 0f
             placeable.place(0, 0, zIndex)
         }
     }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index 5aa3895..91e97da 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -51,14 +51,12 @@
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.shadow
@@ -686,8 +684,8 @@
         enabled = enabled,
         interactionSource = interactionSource,
         onDragStopped = { state.gestureEndAction() },
-        startDragImmediately = state.draggableState.isDragging,
-        state = state.draggableState
+        startDragImmediately = state.isDragging,
+        state = state
     )
 
     Layout(
@@ -848,7 +846,7 @@
             .roundToInt()
         // When start thumb and end thumb have different widths,
         // we need to add a correction for the centering of the slider.
-        val endCorrection = (state.startThumbWidth - state.endThumbWidth) / 2
+        val endCorrection = (startThumbPlaceable.width - endThumbPlaceable.width) / 2
         val endThumbOffsetX =
             (trackPlaceable.width * state.coercedActiveRangeEndAsFraction + endCorrection)
                 .roundToInt()
@@ -1004,7 +1002,7 @@
                 )
                 .hoverable(interactionSource = interactionSource)
                 .shadow(if (enabled) elevation else 0.dp, shape, clip = false)
-                .background(colors.thumbColor(enabled).value, shape)
+                .background(colors.thumbColor(enabled), shape)
         )
     }
 
@@ -1020,7 +1018,9 @@
      * not respond to user input, and it will appear visually disabled and disabled to
      * accessibility services.
      */
+    @Suppress("DEPRECATION")
     @Composable
+    @Deprecated("Use version that supports slider state")
     fun Track(
         sliderPositions: SliderPositions,
         modifier: Modifier = Modifier,
@@ -1044,7 +1044,7 @@
             val tickSize = TickSize.toPx()
             val trackStrokeWidth = TrackHeight.toPx()
             drawLine(
-                inactiveTrackColor.value,
+                inactiveTrackColor,
                 sliderStart,
                 sliderEnd,
                 trackStrokeWidth,
@@ -1063,7 +1063,7 @@
             )
 
             drawLine(
-                activeTrackColor.value,
+                activeTrackColor,
                 sliderValueStart,
                 sliderValueEnd,
                 trackStrokeWidth,
@@ -1078,7 +1078,7 @@
                             Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
                         },
                         PointMode.Points,
-                        (if (outsideFraction) inactiveTickColor else activeTickColor).value,
+                        (if (outsideFraction) inactiveTickColor else activeTickColor),
                         tickSize,
                         StrokeCap.Round
                     )
@@ -1105,10 +1105,10 @@
         colors: SliderColors = colors(),
         enabled: Boolean = true
     ) {
-        val inactiveTrackColor by colors.trackColor(enabled, active = false)
-        val activeTrackColor by colors.trackColor(enabled, active = true)
-        val inactiveTickColor by colors.tickColor(enabled, active = false)
-        val activeTickColor by colors.tickColor(enabled, active = true)
+        val inactiveTrackColor = colors.trackColor(enabled, active = false)
+        val activeTrackColor = colors.trackColor(enabled, active = true)
+        val inactiveTickColor = colors.tickColor(enabled, active = false)
+        val activeTickColor = colors.tickColor(enabled, active = true)
         Canvas(
             modifier
                 .fillMaxWidth()
@@ -1145,10 +1145,10 @@
         colors: SliderColors = colors(),
         enabled: Boolean = true
     ) {
-        val inactiveTrackColor by colors.trackColor(enabled, active = false)
-        val activeTrackColor by colors.trackColor(enabled, active = true)
-        val inactiveTickColor by colors.tickColor(enabled, active = false)
-        val activeTickColor by colors.tickColor(enabled, active = true)
+        val inactiveTrackColor = colors.trackColor(enabled, active = false)
+        val activeTrackColor = colors.trackColor(enabled, active = true)
+        val inactiveTickColor = colors.tickColor(enabled, active = false)
+        val activeTickColor = colors.tickColor(enabled, active = true)
         Canvas(
             modifier
                 .fillMaxWidth()
@@ -1208,18 +1208,13 @@
             trackStrokeWidth,
             StrokeCap.Round
         )
-        tickFractions.groupBy {
-            it > activeRangeEnd ||
-                it < activeRangeStart
-        }.forEach { (outsideFraction, list) ->
-            drawPoints(
-                list.fastMap {
-                    Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
-                },
-                PointMode.Points,
-                (if (outsideFraction) inactiveTickColor else activeTickColor),
-                tickSize,
-                StrokeCap.Round
+
+        for (tick in tickFractions) {
+            val outsideFraction = tick > activeRangeEnd || tick < activeRangeStart
+            drawCircle(
+                color = if (outsideFraction) inactiveTickColor else activeTickColor,
+                center = Offset(lerp(sliderStart, sliderEnd, tick).x, center.y),
+                radius = tickSize / 2f
             )
         }
     }
@@ -1322,12 +1317,10 @@
     state: RangeSliderState,
     enabled: Boolean
 ): Modifier {
-    val valueRange = state.valueRange.start..state.coercedEnd
-    val coerced = state.coercedStart.coerceIn(
-        valueRange.start,
-        valueRange.endInclusive
-    )
+    val valueRange = state.valueRange.start..state.activeRangeEnd
+
     return semantics {
+
         if (!enabled) disabled()
         setProgress(
             action = { targetValue ->
@@ -1356,17 +1349,17 @@
 
                 // This is to keep it consistent with AbsSeekbar.java: return false if no
                 // change from current.
-                if (resolvedValue == coerced) {
+                if (resolvedValue == state.activeRangeStart) {
                     false
                 } else {
-                    state.onValueChange(FloatRange(resolvedValue, state.coercedEnd))
+                    state.onValueChange(FloatRange(resolvedValue, state.activeRangeEnd))
                     state.onValueChangeFinished?.invoke()
                     true
                 }
             }
         )
     }.progressSemantics(
-        state.coercedStart,
+        state.activeRangeStart,
         valueRange,
         state.startSteps
     )
@@ -1377,13 +1370,11 @@
     state: RangeSliderState,
     enabled: Boolean
 ): Modifier {
-    val valueRange = state.coercedStart..state.valueRange.endInclusive
-    val coerced = state.coercedEnd.coerceIn(
-        valueRange.start,
-        valueRange.endInclusive
-    )
+    val valueRange = state.activeRangeStart..state.valueRange.endInclusive
+
     return semantics {
         if (!enabled) disabled()
+
         setProgress(
             action = { targetValue ->
                 var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive)
@@ -1408,17 +1399,17 @@
 
                 // This is to keep it consistent with AbsSeekbar.java: return false if no
                 // change from current.
-                if (resolvedValue == coerced) {
+                if (resolvedValue == state.activeRangeEnd) {
                     false
                 } else {
-                    state.onValueChange(FloatRange(state.coercedStart, resolvedValue))
+                    state.onValueChange(FloatRange(state.activeRangeStart, resolvedValue))
                     state.onValueChangeFinished?.invoke()
                     true
                 }
             }
         )
     }.progressSemantics(
-        state.coercedEnd,
+        state.activeRangeEnd,
         valueRange,
         state.endSteps
     )
@@ -1432,9 +1423,9 @@
 ) = if (enabled) {
     pointerInput(state, interactionSource) {
         detectTapGestures(
-            onPress = state.press,
+            onPress = { with(state) { onPress(it) } },
             onTap = {
-                state.draggableState.dispatchRawDelta(0f)
+                state.dispatchRawDelta(0f)
                 state.gestureEndAction()
             }
         )
@@ -1451,13 +1442,7 @@
     enabled: Boolean
 ): Modifier =
     if (enabled) {
-        pointerInput(
-            startInteractionSource,
-            endInteractionSource,
-            state.totalWidth,
-            state.isRtl,
-            state.valueRange
-        ) {
+        pointerInput(startInteractionSource, endInteractionSource, state) {
             val rangeSliderLogic = RangeSliderLogic(
                 state,
                 startInteractionSource,
@@ -1497,7 +1482,7 @@
                     val finishInteraction = try {
                         val success = horizontalDrag(pointerId = event.id) {
                             val deltaX = it.positionChange().x
-                            state.onDrag.invoke(draggingStart, if (state.isRtl) -deltaX else deltaX)
+                            state.onDrag(draggingStart, if (state.isRtl) -deltaX else deltaX)
                         }
                         if (success) {
                             DragInteraction.Stop(interaction)
@@ -1522,7 +1507,7 @@
     }
 
 @OptIn(ExperimentalMaterial3Api::class)
-private class RangeSliderLogic constructor(
+private class RangeSliderLogic(
     val state: RangeSliderState,
     val startInteractionSource: MutableInteractionSource,
     val endInteractionSource: MutableInteractionSource
@@ -1542,7 +1527,7 @@
         interaction: Interaction,
         scope: CoroutineScope
     ) {
-        state.onDrag.invoke(
+        state.onDrag(
             draggingStart,
             posX - if (draggingStart) state.rawOffsetStart else state.rawOffsetEnd
         )
@@ -1591,33 +1576,22 @@
     val disabledInactiveTrackColor: Color,
     val disabledInactiveTickColor: Color
 ) {
+    internal fun thumbColor(enabled: Boolean): Color =
+        if (enabled) thumbColor else disabledThumbColor
 
-    @Composable
-    internal fun thumbColor(enabled: Boolean): State<Color> {
-        return rememberUpdatedState(if (enabled) thumbColor else disabledThumbColor)
-    }
+    internal fun trackColor(enabled: Boolean, active: Boolean): Color =
+        if (enabled) {
+            if (active) activeTrackColor else inactiveTrackColor
+        } else {
+            if (active) disabledActiveTrackColor else disabledInactiveTrackColor
+        }
 
-    @Composable
-    internal fun trackColor(enabled: Boolean, active: Boolean): State<Color> {
-        return rememberUpdatedState(
-            if (enabled) {
-                if (active) activeTrackColor else inactiveTrackColor
-            } else {
-                if (active) disabledActiveTrackColor else disabledInactiveTrackColor
-            }
-        )
-    }
-
-    @Composable
-    internal fun tickColor(enabled: Boolean, active: Boolean): State<Color> {
-        return rememberUpdatedState(
-            if (enabled) {
-                if (active) activeTickColor else inactiveTickColor
-            } else {
-                if (active) disabledActiveTickColor else disabledInactiveTickColor
-            }
-        )
-    }
+    internal fun tickColor(enabled: Boolean, active: Boolean): Color =
+        if (enabled) {
+            if (active) activeTickColor else inactiveTickColor
+        } else {
+            if (active) disabledActiveTickColor else disabledInactiveTickColor
+        }
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -1663,33 +1637,6 @@
 // Internal to be referred to in tests
 internal val TrackHeight = SliderTokens.InactiveTrackHeight
 
-internal class SliderDraggableState(
-    val onDelta: (Float) -> Unit
-) : DraggableState {
-
-    var isDragging by mutableStateOf(false)
-        private set
-
-    private val dragScope: DragScope = object : DragScope {
-        override fun dragBy(pixels: Float): Unit = onDelta(pixels)
-    }
-
-    private val scrollMutex = MutatorMutex()
-
-    override suspend fun drag(
-        dragPriority: MutatePriority,
-        block: suspend DragScope.() -> Unit
-    ): Unit = coroutineScope {
-        isDragging = true
-        scrollMutex.mutateWith(dragScope, dragPriority, block)
-        isDragging = false
-    }
-
-    override fun dispatchRawDelta(delta: Float) {
-        return onDelta(delta)
-    }
-}
-
 private enum class SliderComponents {
     THUMB,
     TRACK
@@ -1705,6 +1652,8 @@
  * Class that holds information about [Slider]'s and [RangeSlider]'s active track
  * and fractional positions where the discrete ticks should be drawn on the track.
  */
+@Suppress("DEPRECATION")
+@Deprecated("Not necessary with the introduction of Slider state")
 @Stable
 class SliderPositions(
     initialActiveRange: ClosedFloatingPointRange<Float> = 0f..1f,
@@ -1767,7 +1716,8 @@
     val steps: Int = 0,
     val valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
     var onValueChangeFinished: (() -> Unit)? = null
-) {
+) : DraggableState {
+
     private var valueState by mutableFloatStateOf(initialValue)
 
     /**
@@ -1787,6 +1737,24 @@
         }
         get() = valueState
 
+    override suspend fun drag(
+        dragPriority: MutatePriority,
+        block: suspend DragScope.() -> Unit
+    ): Unit = coroutineScope {
+        isDragging = true
+        scrollMutex.mutateWith(dragScope, dragPriority, block)
+        isDragging = false
+    }
+
+    override fun dispatchRawDelta(delta: Float) {
+        val maxPx = max(totalWidth - thumbWidth / 2, 0f)
+        val minPx = min(thumbWidth / 2, maxPx)
+        rawOffset = (rawOffset + delta + pressOffset)
+        pressOffset = 0f
+        val offsetInTrack = snapValueToTick(rawOffset, tickFractions, minPx, maxPx)
+        onValueChange(scaleToUserValue(minPx, maxPx, offsetInTrack))
+    }
+
     /**
      * callback in which value should be updated
      */
@@ -1797,12 +1765,7 @@
     }
 
     internal val tickFractions = stepsToTickFractions(steps)
-
     internal var totalWidth by mutableIntStateOf(0)
-
-    private var rawOffset by mutableFloatStateOf(scaleToOffset(0f, 0f, value))
-    private var pressOffset by mutableFloatStateOf(0f)
-
     internal var isRtl = false
     internal var thumbWidth by mutableFloatStateOf(0f)
 
@@ -1813,24 +1776,25 @@
             value.coerceIn(valueRange.start, valueRange.endInclusive)
         )
 
-    internal val draggableState =
-        SliderDraggableState {
-            val maxPx = max(totalWidth - thumbWidth / 2, 0f)
-            val minPx = min(thumbWidth / 2, maxPx)
-            rawOffset = (rawOffset + it + pressOffset)
-            pressOffset = 0f
-            val offsetInTrack = snapValueToTick(rawOffset, tickFractions, minPx, maxPx)
-            onValueChange(scaleToUserValue(minPx, maxPx, offsetInTrack))
-        }
+    internal var isDragging by mutableStateOf(false)
+        private set
+
+    internal fun updateDimensions(
+        newThumbWidth: Float,
+        newTotalWidth: Int
+    ) {
+        thumbWidth = newThumbWidth
+        totalWidth = newTotalWidth
+    }
 
     internal val gestureEndAction = {
-        if (!draggableState.isDragging) {
+        if (!isDragging) {
             // check isDragging in case the change is still in progress (touch -> drag case)
             onValueChangeFinished?.invoke()
         }
     }
 
-    internal val press: suspend PressGestureScope.(Offset) -> Unit = { pos ->
+    internal suspend fun PressGestureScope.onPress(pos: Offset) {
         val to = if (isRtl) totalWidth - pos.x else pos.x
         pressOffset = to - rawOffset
         try {
@@ -1840,14 +1804,14 @@
         }
     }
 
-    internal fun updateDimensions(
-        newThumbWidth: Float,
-        newTotalWidth: Int
-    ) {
-        thumbWidth = newThumbWidth
-        totalWidth = newTotalWidth
+    private var rawOffset by mutableFloatStateOf(scaleToOffset(0f, 0f, value))
+    private var pressOffset by mutableFloatStateOf(0f)
+    private val dragScope: DragScope = object : DragScope {
+        override fun dragBy(pixels: Float): Unit = dispatchRawDelta(pixels)
     }
 
+    private val scrollMutex = MutatorMutex()
+
     private fun defaultOnValueChange(newVal: Float) { value = newVal }
 
     private fun scaleToUserValue(minPx: Float, maxPx: Float, offset: Float) =
@@ -1907,6 +1871,7 @@
             activeRangeStartState = snappedValue
         }
         get() = activeRangeStartState
+
     var activeRangeEnd: Float
         set(newVal) {
             val coercedValue = newVal.coerceIn(activeRangeStart, valueRange.endInclusive)
@@ -1928,23 +1893,22 @@
 
     internal val tickFractions = stepsToTickFractions(steps)
 
-    internal var startThumbWidth by mutableFloatStateOf(ThumbWidth.value)
-    internal var endThumbWidth by mutableFloatStateOf(ThumbWidth.value)
+    internal var startThumbWidth by mutableFloatStateOf(0f)
+    internal var endThumbWidth by mutableFloatStateOf(0f)
     internal var totalWidth by mutableIntStateOf(0)
-
     internal var rawOffsetStart by mutableFloatStateOf(0f)
     internal var rawOffsetEnd by mutableFloatStateOf(0f)
 
-    internal var isRtl = false
+    internal var isRtl by mutableStateOf(false)
 
     internal val gestureEndAction: (Boolean) -> Unit = {
         onValueChangeFinished?.invoke()
     }
 
-    private var maxPx by mutableFloatStateOf(max(totalWidth - endThumbWidth / 2, 0f))
-    private var minPx by mutableFloatStateOf(min(startThumbWidth / 2, maxPx))
+    private var maxPx by mutableFloatStateOf(0f)
+    private var minPx by mutableFloatStateOf(0f)
 
-    internal val onDrag: (Boolean, Float) -> Unit = { isStart, offset ->
+    internal fun onDrag(isStart: Boolean, offset: Float) {
         val offsetRange = if (isStart) {
             rawOffsetStart = (rawOffsetStart + offset)
             rawOffsetEnd = scaleToOffset(minPx, maxPx, activeRangeEnd)
@@ -1963,24 +1927,18 @@
         onValueChange(scaleToUserValue(minPx, maxPx, offsetRange))
     }
 
-    internal val coercedStart
-        get() = activeRangeStart.coerceIn(valueRange.start, activeRangeEnd)
-
-    internal val coercedEnd
-        get() = activeRangeEnd.coerceIn(activeRangeStart, valueRange.endInclusive)
-
     internal val coercedActiveRangeStartAsFraction
         get() = calcFraction(
             valueRange.start,
             valueRange.endInclusive,
-            coercedStart
+            activeRangeStart
         )
 
     internal val coercedActiveRangeEndAsFraction
         get() = calcFraction(
             valueRange.start,
             valueRange.endInclusive,
-            coercedEnd
+            activeRangeEnd
         )
 
     internal val startSteps
@@ -2007,7 +1965,7 @@
 
     internal fun updateMinMaxPx() {
         val newMaxPx = max(totalWidth - endThumbWidth / 2, 0f)
-        val newMinPx = min(startThumbWidth / 2, maxPx)
+        val newMinPx = min(startThumbWidth / 2, newMaxPx)
         if (minPx != newMinPx || maxPx != newMaxPx) {
             minPx = newMinPx
             maxPx = newMaxPx
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 66bd67f..9416825 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -378,16 +378,16 @@
 
   public final class ColorKt {
     method @androidx.compose.runtime.Stable public static long Color(float red, float green, float blue, optional float alpha, optional androidx.compose.ui.graphics.colorspace.ColorSpace colorSpace);
-    method @androidx.compose.runtime.Stable public static long Color(@ColorInt int color);
-    method @androidx.compose.runtime.Stable public static long Color(@IntRange(from=0L, to=255L) int red, @IntRange(from=0L, to=255L) int green, @IntRange(from=0L, to=255L) int blue, optional @IntRange(from=0L, to=255L) int alpha);
+    method @androidx.compose.runtime.Stable public static long Color(int color);
+    method @androidx.compose.runtime.Stable public static long Color(int red, int green, int blue, optional int alpha);
     method @androidx.compose.runtime.Stable public static long Color(long color);
     method @androidx.compose.runtime.Stable public static long compositeOver(long, long background);
     method public static inline boolean isSpecified(long);
     method public static inline boolean isUnspecified(long);
-    method @androidx.compose.runtime.Stable public static long lerp(long start, long stop, @FloatRange(from=0.0, to=1.0) float fraction);
+    method @androidx.compose.runtime.Stable public static long lerp(long start, long stop, float fraction);
     method @androidx.compose.runtime.Stable public static float luminance(long);
     method public static inline long takeOrElse(long, kotlin.jvm.functions.Function0<androidx.compose.ui.graphics.Color> block);
-    method @ColorInt @androidx.compose.runtime.Stable public static int toArgb(long);
+    method @androidx.compose.runtime.Stable public static int toArgb(long);
   }
 
   @kotlin.jvm.JvmInline public final value class ColorMatrix {
@@ -569,8 +569,8 @@
   public final class OutlineKt {
     method public static void addOutline(androidx.compose.ui.graphics.Path, androidx.compose.ui.graphics.Outline outline);
     method public static void drawOutline(androidx.compose.ui.graphics.Canvas, androidx.compose.ui.graphics.Outline outline, androidx.compose.ui.graphics.Paint paint);
-    method public static void drawOutline(androidx.compose.ui.graphics.drawscope.DrawScope, androidx.compose.ui.graphics.Outline outline, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public static void drawOutline(androidx.compose.ui.graphics.drawscope.DrawScope, androidx.compose.ui.graphics.Outline outline, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public static void drawOutline(androidx.compose.ui.graphics.drawscope.DrawScope, androidx.compose.ui.graphics.Outline outline, androidx.compose.ui.graphics.Brush brush, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public static void drawOutline(androidx.compose.ui.graphics.drawscope.DrawScope, androidx.compose.ui.graphics.Outline outline, long color, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
   }
 
   public interface Paint {
@@ -728,7 +728,7 @@
 
   public final class PixelMap {
     ctor public PixelMap(int[] buffer, int width, int height, int bufferOffset, int stride);
-    method public operator long get(@IntRange(from=0L) int x, @IntRange(from=0L) int y);
+    method public operator long get(int x, int y);
     method public int[] getBuffer();
     method public int getBufferOffset();
     method public int getHeight();
@@ -931,8 +931,8 @@
   }
 
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ColorModel {
-    method @IntRange(from=1L, to=4L) public int getComponentCount();
-    property @IntRange(from=1L, to=4L) @androidx.compose.runtime.Stable public final int componentCount;
+    method public int getComponentCount();
+    property @androidx.compose.runtime.Stable public final int componentCount;
     field public static final androidx.compose.ui.graphics.colorspace.ColorModel.Companion Companion;
   }
 
@@ -949,18 +949,18 @@
 
   public abstract class ColorSpace {
     ctor public ColorSpace(String name, long model);
-    method @Size(min=3L) public final float[] fromXyz(float x, float y, float z);
-    method @Size(min=3L) public abstract float[] fromXyz(@Size(min=3L) float[] v);
-    method @IntRange(from=1L, to=4L) public final int getComponentCount();
-    method public abstract float getMaxValue(@IntRange(from=0L, to=3L) int component);
-    method public abstract float getMinValue(@IntRange(from=0L, to=3L) int component);
+    method public final float[] fromXyz(float x, float y, float z);
+    method public abstract float[] fromXyz(float[] v);
+    method public final int getComponentCount();
+    method public abstract float getMaxValue(int component);
+    method public abstract float getMinValue(int component);
     method public final long getModel();
     method public final String getName();
     method public boolean isSrgb();
     method public abstract boolean isWideGamut();
-    method @Size(3L) public final float[] toXyz(float r, float g, float b);
-    method @Size(min=3L) public abstract float[] toXyz(@Size(min=3L) float[] v);
-    property @IntRange(from=1L, to=4L) public final int componentCount;
+    method public final float[] toXyz(float r, float g, float b);
+    method public abstract float[] toXyz(float[] v);
+    property public final int componentCount;
     property public boolean isSrgb;
     property public abstract boolean isWideGamut;
     property public final long model;
@@ -991,7 +991,7 @@
     method public androidx.compose.ui.graphics.colorspace.Rgb getProPhotoRgb();
     method public androidx.compose.ui.graphics.colorspace.Rgb getSmpteC();
     method public androidx.compose.ui.graphics.colorspace.Rgb getSrgb();
-    method public androidx.compose.ui.graphics.colorspace.ColorSpace? match(@Size(9L) float[] toXYZD50, androidx.compose.ui.graphics.colorspace.TransferParameters function);
+    method public androidx.compose.ui.graphics.colorspace.ColorSpace? match(float[] toXYZD50, androidx.compose.ui.graphics.colorspace.TransferParameters function);
     property public final androidx.compose.ui.graphics.colorspace.Rgb Aces;
     property public final androidx.compose.ui.graphics.colorspace.Rgb Acescg;
     property public final androidx.compose.ui.graphics.colorspace.Rgb AdobeRgb;
@@ -1016,8 +1016,8 @@
     method public final androidx.compose.ui.graphics.colorspace.ColorSpace getDestination();
     method public final int getRenderIntent();
     method public final androidx.compose.ui.graphics.colorspace.ColorSpace getSource();
-    method @Size(3L) public final float[] transform(float r, float g, float b);
-    method @Size(min=3L) public float[] transform(@Size(min=3L) float[] v);
+    method public final float[] transform(float r, float g, float b);
+    method public float[] transform(float[] v);
     property public final androidx.compose.ui.graphics.colorspace.ColorSpace destination;
     property public final int renderIntent;
     property public final androidx.compose.ui.graphics.colorspace.ColorSpace source;
@@ -1061,30 +1061,30 @@
   }
 
   public final class Rgb extends androidx.compose.ui.graphics.colorspace.ColorSpace {
-    ctor public Rgb(@Size(min=1L) String name, @Size(9L) float[] toXYZ, androidx.compose.ui.graphics.colorspace.TransferParameters function);
-    ctor public Rgb(@Size(min=1L) String name, @Size(min=6L, max=9L) float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, androidx.compose.ui.graphics.colorspace.TransferParameters function);
-    ctor public Rgb(@Size(min=1L) String name, @Size(min=6L, max=9L) float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, double gamma);
-    ctor public Rgb(@Size(min=1L) String name, @Size(min=6L, max=9L) float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> oetf, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> eotf, float min, float max);
-    ctor public Rgb(@Size(min=1L) String name, @Size(9L) float[] toXYZ, double gamma);
-    ctor public Rgb(@Size(min=1L) String name, @Size(9L) float[] toXYZ, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> oetf, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> eotf);
-    method @Size(3L) public float[] fromLinear(float r, float g, float b);
-    method @Size(min=3L) public float[] fromLinear(@Size(min=3L) float[] v);
+    ctor public Rgb(String name, float[] toXYZ, androidx.compose.ui.graphics.colorspace.TransferParameters function);
+    ctor public Rgb(String name, float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, androidx.compose.ui.graphics.colorspace.TransferParameters function);
+    ctor public Rgb(String name, float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, double gamma);
+    ctor public Rgb(String name, float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> oetf, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> eotf, float min, float max);
+    ctor public Rgb(String name, float[] toXYZ, double gamma);
+    ctor public Rgb(String name, float[] toXYZ, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> oetf, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> eotf);
+    method public float[] fromLinear(float r, float g, float b);
+    method public float[] fromLinear(float[] v);
     method public float[] fromXyz(float[] v);
     method public kotlin.jvm.functions.Function1<java.lang.Double,java.lang.Double> getEotf();
-    method @Size(9L) public float[] getInverseTransform();
-    method @Size(min=9L) public float[] getInverseTransform(@Size(min=9L) float[] inverseTransform);
+    method public float[] getInverseTransform();
+    method public float[] getInverseTransform(float[] inverseTransform);
     method public float getMaxValue(int component);
     method public float getMinValue(int component);
     method public kotlin.jvm.functions.Function1<java.lang.Double,java.lang.Double> getOetf();
-    method @Size(6L) public float[] getPrimaries();
-    method @Size(min=6L) public float[] getPrimaries(@Size(min=6L) float[] primaries);
+    method public float[] getPrimaries();
+    method public float[] getPrimaries(float[] primaries);
     method public androidx.compose.ui.graphics.colorspace.TransferParameters? getTransferParameters();
-    method @Size(9L) public float[] getTransform();
-    method @Size(min=9L) public float[] getTransform(@Size(min=9L) float[] transform);
+    method public float[] getTransform();
+    method public float[] getTransform(float[] transform);
     method public androidx.compose.ui.graphics.colorspace.WhitePoint getWhitePoint();
     method public boolean isWideGamut();
-    method @Size(3L) public float[] toLinear(float r, float g, float b);
-    method @Size(min=3L) public float[] toLinear(@Size(min=3L) float[] v);
+    method public float[] toLinear(float r, float g, float b);
+    method public float[] toLinear(float[] v);
     method public float[] toXyz(float[] v);
     property public final kotlin.jvm.functions.Function1<java.lang.Double,java.lang.Double> eotf;
     property public boolean isSrgb;
@@ -1139,24 +1139,24 @@
   public final class CanvasDrawScope implements androidx.compose.ui.graphics.drawscope.DrawScope {
     ctor public CanvasDrawScope();
     method public inline void draw(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.graphics.Canvas canvas, long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
-    method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawCircle(androidx.compose.ui.graphics.Brush brush, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawCircle(long color, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method @Deprecated public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawLine(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawLine(long color, long start, long end, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawOval(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawOval(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawPath(androidx.compose.ui.graphics.Path path, long color, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, long color, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawRect(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawRect(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawRoundRect(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, long cornerRadius, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawRoundRect(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawCircle(androidx.compose.ui.graphics.Brush brush, float radius, long center, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawCircle(long color, float radius, long center, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method @Deprecated public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawLine(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawLine(long color, long start, long end, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawOval(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawOval(long color, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawPath(androidx.compose.ui.graphics.Path path, long color, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, long color, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawRect(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawRect(long color, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawRoundRect(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, long cornerRadius, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawRoundRect(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
     method public float getDensity();
     method public androidx.compose.ui.graphics.drawscope.DrawContext getDrawContext();
     method public float getFontScale();
@@ -1189,25 +1189,25 @@
   }
 
   @androidx.compose.ui.graphics.drawscope.DrawScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface DrawScope extends androidx.compose.ui.unit.Density {
-    method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawCircle(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawCircle(long color, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method @Deprecated public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public default void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode, optional int filterQuality);
-    method public void drawLine(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawLine(long color, long start, long end, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawOval(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawOval(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawPath(androidx.compose.ui.graphics.Path path, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, long color, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawRect(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawRect(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawRoundRect(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional long cornerRadius, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawRoundRect(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawCircle(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawCircle(long color, optional float radius, optional long center, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method @Deprecated public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public default void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode, optional int filterQuality);
+    method public void drawLine(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawLine(long color, long start, long end, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawOval(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawOval(long color, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawPath(androidx.compose.ui.graphics.Path path, long color, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, long color, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawRect(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawRect(long color, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawRoundRect(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional long cornerRadius, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawRoundRect(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
     method public default long getCenter();
     method public androidx.compose.ui.graphics.drawscope.DrawContext getDrawContext();
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index ecfd67e..935e1f5 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -409,16 +409,16 @@
 
   public final class ColorKt {
     method @androidx.compose.runtime.Stable public static long Color(float red, float green, float blue, optional float alpha, optional androidx.compose.ui.graphics.colorspace.ColorSpace colorSpace);
-    method @androidx.compose.runtime.Stable public static long Color(@ColorInt int color);
-    method @androidx.compose.runtime.Stable public static long Color(@IntRange(from=0L, to=255L) int red, @IntRange(from=0L, to=255L) int green, @IntRange(from=0L, to=255L) int blue, optional @IntRange(from=0L, to=255L) int alpha);
+    method @androidx.compose.runtime.Stable public static long Color(int color);
+    method @androidx.compose.runtime.Stable public static long Color(int red, int green, int blue, optional int alpha);
     method @androidx.compose.runtime.Stable public static long Color(long color);
     method @androidx.compose.runtime.Stable public static long compositeOver(long, long background);
     method public static inline boolean isSpecified(long);
     method public static inline boolean isUnspecified(long);
-    method @androidx.compose.runtime.Stable public static long lerp(long start, long stop, @FloatRange(from=0.0, to=1.0) float fraction);
+    method @androidx.compose.runtime.Stable public static long lerp(long start, long stop, float fraction);
     method @androidx.compose.runtime.Stable public static float luminance(long);
     method public static inline long takeOrElse(long, kotlin.jvm.functions.Function0<androidx.compose.ui.graphics.Color> block);
-    method @ColorInt @androidx.compose.runtime.Stable public static int toArgb(long);
+    method @androidx.compose.runtime.Stable public static int toArgb(long);
   }
 
   @kotlin.jvm.JvmInline public final value class ColorMatrix {
@@ -604,8 +604,8 @@
   public final class OutlineKt {
     method public static void addOutline(androidx.compose.ui.graphics.Path, androidx.compose.ui.graphics.Outline outline);
     method public static void drawOutline(androidx.compose.ui.graphics.Canvas, androidx.compose.ui.graphics.Outline outline, androidx.compose.ui.graphics.Paint paint);
-    method public static void drawOutline(androidx.compose.ui.graphics.drawscope.DrawScope, androidx.compose.ui.graphics.Outline outline, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public static void drawOutline(androidx.compose.ui.graphics.drawscope.DrawScope, androidx.compose.ui.graphics.Outline outline, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public static void drawOutline(androidx.compose.ui.graphics.drawscope.DrawScope, androidx.compose.ui.graphics.Outline outline, androidx.compose.ui.graphics.Brush brush, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public static void drawOutline(androidx.compose.ui.graphics.drawscope.DrawScope, androidx.compose.ui.graphics.Outline outline, long color, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
   }
 
   public interface Paint {
@@ -763,7 +763,7 @@
 
   public final class PixelMap {
     ctor public PixelMap(int[] buffer, int width, int height, int bufferOffset, int stride);
-    method public operator long get(@IntRange(from=0L) int x, @IntRange(from=0L) int y);
+    method public operator long get(int x, int y);
     method public int[] getBuffer();
     method public int getBufferOffset();
     method public int getHeight();
@@ -966,8 +966,8 @@
   }
 
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ColorModel {
-    method @IntRange(from=1L, to=4L) public int getComponentCount();
-    property @IntRange(from=1L, to=4L) @androidx.compose.runtime.Stable public final int componentCount;
+    method public int getComponentCount();
+    property @androidx.compose.runtime.Stable public final int componentCount;
     field public static final androidx.compose.ui.graphics.colorspace.ColorModel.Companion Companion;
   }
 
@@ -984,18 +984,18 @@
 
   public abstract class ColorSpace {
     ctor public ColorSpace(String name, long model);
-    method @Size(min=3L) public final float[] fromXyz(float x, float y, float z);
-    method @Size(min=3L) public abstract float[] fromXyz(@Size(min=3L) float[] v);
-    method @IntRange(from=1L, to=4L) public final int getComponentCount();
-    method public abstract float getMaxValue(@IntRange(from=0L, to=3L) int component);
-    method public abstract float getMinValue(@IntRange(from=0L, to=3L) int component);
+    method public final float[] fromXyz(float x, float y, float z);
+    method public abstract float[] fromXyz(float[] v);
+    method public final int getComponentCount();
+    method public abstract float getMaxValue(int component);
+    method public abstract float getMinValue(int component);
     method public final long getModel();
     method public final String getName();
     method public boolean isSrgb();
     method public abstract boolean isWideGamut();
-    method @Size(3L) public final float[] toXyz(float r, float g, float b);
-    method @Size(min=3L) public abstract float[] toXyz(@Size(min=3L) float[] v);
-    property @IntRange(from=1L, to=4L) public final int componentCount;
+    method public final float[] toXyz(float r, float g, float b);
+    method public abstract float[] toXyz(float[] v);
+    property public final int componentCount;
     property public boolean isSrgb;
     property public abstract boolean isWideGamut;
     property public final long model;
@@ -1026,7 +1026,7 @@
     method public androidx.compose.ui.graphics.colorspace.Rgb getProPhotoRgb();
     method public androidx.compose.ui.graphics.colorspace.Rgb getSmpteC();
     method public androidx.compose.ui.graphics.colorspace.Rgb getSrgb();
-    method public androidx.compose.ui.graphics.colorspace.ColorSpace? match(@Size(9L) float[] toXYZD50, androidx.compose.ui.graphics.colorspace.TransferParameters function);
+    method public androidx.compose.ui.graphics.colorspace.ColorSpace? match(float[] toXYZD50, androidx.compose.ui.graphics.colorspace.TransferParameters function);
     property public final androidx.compose.ui.graphics.colorspace.Rgb Aces;
     property public final androidx.compose.ui.graphics.colorspace.Rgb Acescg;
     property public final androidx.compose.ui.graphics.colorspace.Rgb AdobeRgb;
@@ -1051,8 +1051,8 @@
     method public final androidx.compose.ui.graphics.colorspace.ColorSpace getDestination();
     method public final int getRenderIntent();
     method public final androidx.compose.ui.graphics.colorspace.ColorSpace getSource();
-    method @Size(3L) public final float[] transform(float r, float g, float b);
-    method @Size(min=3L) public float[] transform(@Size(min=3L) float[] v);
+    method public final float[] transform(float r, float g, float b);
+    method public float[] transform(float[] v);
     property public final androidx.compose.ui.graphics.colorspace.ColorSpace destination;
     property public final int renderIntent;
     property public final androidx.compose.ui.graphics.colorspace.ColorSpace source;
@@ -1096,30 +1096,30 @@
   }
 
   public final class Rgb extends androidx.compose.ui.graphics.colorspace.ColorSpace {
-    ctor public Rgb(@Size(min=1L) String name, @Size(9L) float[] toXYZ, androidx.compose.ui.graphics.colorspace.TransferParameters function);
-    ctor public Rgb(@Size(min=1L) String name, @Size(min=6L, max=9L) float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, androidx.compose.ui.graphics.colorspace.TransferParameters function);
-    ctor public Rgb(@Size(min=1L) String name, @Size(min=6L, max=9L) float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, double gamma);
-    ctor public Rgb(@Size(min=1L) String name, @Size(min=6L, max=9L) float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> oetf, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> eotf, float min, float max);
-    ctor public Rgb(@Size(min=1L) String name, @Size(9L) float[] toXYZ, double gamma);
-    ctor public Rgb(@Size(min=1L) String name, @Size(9L) float[] toXYZ, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> oetf, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> eotf);
-    method @Size(3L) public float[] fromLinear(float r, float g, float b);
-    method @Size(min=3L) public float[] fromLinear(@Size(min=3L) float[] v);
+    ctor public Rgb(String name, float[] toXYZ, androidx.compose.ui.graphics.colorspace.TransferParameters function);
+    ctor public Rgb(String name, float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, androidx.compose.ui.graphics.colorspace.TransferParameters function);
+    ctor public Rgb(String name, float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, double gamma);
+    ctor public Rgb(String name, float[] primaries, androidx.compose.ui.graphics.colorspace.WhitePoint whitePoint, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> oetf, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> eotf, float min, float max);
+    ctor public Rgb(String name, float[] toXYZ, double gamma);
+    ctor public Rgb(String name, float[] toXYZ, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> oetf, kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Double> eotf);
+    method public float[] fromLinear(float r, float g, float b);
+    method public float[] fromLinear(float[] v);
     method public float[] fromXyz(float[] v);
     method public kotlin.jvm.functions.Function1<java.lang.Double,java.lang.Double> getEotf();
-    method @Size(9L) public float[] getInverseTransform();
-    method @Size(min=9L) public float[] getInverseTransform(@Size(min=9L) float[] inverseTransform);
+    method public float[] getInverseTransform();
+    method public float[] getInverseTransform(float[] inverseTransform);
     method public float getMaxValue(int component);
     method public float getMinValue(int component);
     method public kotlin.jvm.functions.Function1<java.lang.Double,java.lang.Double> getOetf();
-    method @Size(6L) public float[] getPrimaries();
-    method @Size(min=6L) public float[] getPrimaries(@Size(min=6L) float[] primaries);
+    method public float[] getPrimaries();
+    method public float[] getPrimaries(float[] primaries);
     method public androidx.compose.ui.graphics.colorspace.TransferParameters? getTransferParameters();
-    method @Size(9L) public float[] getTransform();
-    method @Size(min=9L) public float[] getTransform(@Size(min=9L) float[] transform);
+    method public float[] getTransform();
+    method public float[] getTransform(float[] transform);
     method public androidx.compose.ui.graphics.colorspace.WhitePoint getWhitePoint();
     method public boolean isWideGamut();
-    method @Size(3L) public float[] toLinear(float r, float g, float b);
-    method @Size(min=3L) public float[] toLinear(@Size(min=3L) float[] v);
+    method public float[] toLinear(float r, float g, float b);
+    method public float[] toLinear(float[] v);
     method public float[] toXyz(float[] v);
     property public final kotlin.jvm.functions.Function1<java.lang.Double,java.lang.Double> eotf;
     property public boolean isSrgb;
@@ -1174,24 +1174,24 @@
   public final class CanvasDrawScope implements androidx.compose.ui.graphics.drawscope.DrawScope {
     ctor public CanvasDrawScope();
     method public inline void draw(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.graphics.Canvas canvas, long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
-    method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawCircle(androidx.compose.ui.graphics.Brush brush, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawCircle(long color, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method @Deprecated public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawLine(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawLine(long color, long start, long end, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawOval(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawOval(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawPath(androidx.compose.ui.graphics.Path path, long color, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, long color, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawRect(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawRect(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawRoundRect(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, long cornerRadius, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
-    method public void drawRoundRect(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawCircle(androidx.compose.ui.graphics.Brush brush, float radius, long center, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawCircle(long color, float radius, long center, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method @Deprecated public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawLine(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawLine(long color, long start, long end, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawOval(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawOval(long color, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawPath(androidx.compose.ui.graphics.Path path, long color, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, long color, float strokeWidth, int cap, androidx.compose.ui.graphics.PathEffect? pathEffect, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawRect(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawRect(long color, long topLeft, long size, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawRoundRect(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, long cornerRadius, float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
+    method public void drawRoundRect(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, int blendMode);
     method public float getDensity();
     method public androidx.compose.ui.graphics.drawscope.DrawContext getDrawContext();
     method public float getFontScale();
@@ -1248,25 +1248,25 @@
   }
 
   @androidx.compose.ui.graphics.drawscope.DrawScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface DrawScope extends androidx.compose.ui.unit.Density {
-    method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawCircle(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawCircle(long color, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method @Deprecated public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public default void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode, optional int filterQuality);
-    method public void drawLine(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawLine(long color, long start, long end, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawOval(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawOval(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawPath(androidx.compose.ui.graphics.Path path, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, long color, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawRect(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawRect(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawRoundRect(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional long cornerRadius, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
-    method public void drawRoundRect(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawCircle(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawCircle(long color, optional float radius, optional long center, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method @Deprecated public void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public default void drawImage(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode, optional int filterQuality);
+    method public void drawLine(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawLine(long color, long start, long end, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawOval(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawOval(long color, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawPath(androidx.compose.ui.graphics.Path path, long color, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, int pointMode, long color, optional float strokeWidth, optional int cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawRect(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawRect(long color, optional long topLeft, optional long size, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawRoundRect(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional long cornerRadius, optional float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
+    method public void drawRoundRect(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
     method public default long getCenter();
     method public androidx.compose.ui.graphics.drawscope.DrawContext getDrawContext();
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index 760efaf..3bb74e1 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -36,7 +36,6 @@
         commonMain {
             dependencies {
                 implementation(libs.kotlinStdlibCommon)
-                implementation(project(":annotation:annotation"))
 
                 api(project(":compose:ui:ui-unit"))
                 implementation(project(":compose:runtime:runtime"))
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
index 51b9f61e..0ce5481 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
@@ -16,10 +16,6 @@
 
 package androidx.compose.ui.graphics
 
-import androidx.annotation.ColorInt
-import androidx.annotation.FloatRange
-import androidx.annotation.IntRange
-import androidx.annotation.Size
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.graphics.colorspace.ColorModel
@@ -466,7 +462,7 @@
  * @return A non-null instance of {@link Color}
  */
 @Stable
-fun Color(@ColorInt color: Int): Color {
+fun Color(/*@ColorInt*/ color: Int): Color {
     return Color(value = color.toULong() shl 32)
 }
 
@@ -501,10 +497,14 @@
  */
 @Stable
 fun Color(
-    @IntRange(from = 0, to = 0xFF) red: Int,
-    @IntRange(from = 0, to = 0xFF) green: Int,
-    @IntRange(from = 0, to = 0xFF) blue: Int,
-    @IntRange(from = 0, to = 0xFF) alpha: Int = 0xFF
+    /*@IntRange(from = 0, to = 0xFF)*/
+    red: Int,
+    /*@IntRange(from = 0, to = 0xFF)*/
+    green: Int,
+    /*@IntRange(from = 0, to = 0xFF)*/
+    blue: Int,
+    /*@IntRange(from = 0, to = 0xFF)*/
+    alpha: Int = 0xFF
 ): Color {
     val color = ((alpha and 0xFF) shl 24) or
         ((red and 0xFF) shl 16) or
@@ -520,7 +520,7 @@
  * in the [ColorSpaces.Oklab] color space.
  */
 @Stable
-fun lerp(start: Color, stop: Color, @FloatRange(from = 0.0, to = 1.0) fraction: Float): Color {
+fun lerp(start: Color, stop: Color, /*@FloatRange(from = 0.0, to = 1.0)*/ fraction: Float): Color {
     val colorSpace = ColorSpaces.Oklab
     val startColor = start.convert(colorSpace)
     val endColor = stop.convert(colorSpace)
@@ -592,7 +592,7 @@
  *
  * @return A new, non-null array whose size is 4
  */
-@Size(value = 4)
+/*@Size(value = 4)*/
 private fun Color.getComponents(): FloatArray = floatArrayOf(red, green, blue, alpha)
 
 /**
@@ -634,7 +634,7 @@
  * @return An ARGB color in the sRGB color space
  */
 @Stable
-@ColorInt
+// @ColorInt
 fun Color.toArgb(): Int {
     return (convert(ColorSpaces.Srgb).value shr 32).toInt()
 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
index 3aeac0d..40425a0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.graphics
 
-import androidx.annotation.FloatRange
 import androidx.compose.runtime.Immutable
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.Offset
@@ -148,7 +147,8 @@
 fun DrawScope.drawOutline(
     outline: Outline,
     color: Color,
-    @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+    /*@FloatRange(from = 0.0, to = 1.0)*/
+    alpha: Float = 1.0f,
     style: DrawStyle = Fill,
     colorFilter: ColorFilter? = null,
     blendMode: BlendMode = DrawScope.DefaultBlendMode
@@ -187,7 +187,8 @@
 fun DrawScope.drawOutline(
     outline: Outline,
     brush: Brush,
-    @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+    /*@FloatRange(from = 0.0, to = 1.0)*/
+    alpha: Float = 1.0f,
     style: DrawStyle = Fill,
     colorFilter: ColorFilter? = null,
     blendMode: BlendMode = DrawScope.DefaultBlendMode
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt
index 9cd8e1a..8212167 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt
@@ -16,8 +16,6 @@
 
 package androidx.compose.ui.graphics
 
-import androidx.annotation.IntRange
-
 /**
  * Result of a pixel read operation. This contains the [ImageBitmap] pixel information represented
  * as a 1 dimensional array of values that supports queries of pixel values based on the 2
@@ -48,7 +46,9 @@
      * @param y the vertical pixel coordinate, minimum 1
      */
     operator fun get(
-        @IntRange(from = 0) x: Int,
-        @IntRange(from = 0) y: Int
+        /*@IntRange(from = 0)*/
+        x: Int,
+        /*@IntRange(from = 0)*/
+        y: Int
     ): Color = Color(buffer[bufferOffset + y * stride + x])
 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorModel.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorModel.kt
index eb65cf6..8e71d85 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorModel.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorModel.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.graphics.colorspace
 
-import androidx.annotation.IntRange
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.util.packInts
@@ -42,7 +41,7 @@
      *
      * @return An integer between 1 and 4
      */
-    @get:IntRange(from = 1, to = 4)
+    /*@IntRange(from = 1, to = 4)*/
     @Stable
     val componentCount: Int
         get() {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt
index 3df385d..a754e16 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt
@@ -15,8 +15,6 @@
  */
 package androidx.compose.ui.graphics.colorspace
 
-import androidx.annotation.IntRange
-import androidx.annotation.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.util.packFloats
 import kotlin.math.abs
@@ -152,7 +150,7 @@
      * @see model
      */
     val componentCount: Int
-        @IntRange(from = 1, to = 4)
+        /*@IntRange(from = 1, to = 4)*/
         get() = model.componentCount
 
     /**
@@ -221,7 +219,7 @@
      * @see getMaxValue
      * @see ColorModel.componentCount
      */
-    abstract fun getMinValue(@IntRange(from = 0, to = 3) component: Int): Float
+    abstract fun getMinValue(/*@IntRange(from = 0, to = 3)*/ component: Int): Float
 
     /**
      * Returns the maximum valid value for the specified component of this
@@ -233,7 +231,7 @@
      * @see getMinValue
      * @see ColorModel.componentCount
      */
-    abstract fun getMaxValue(@IntRange(from = 0, to = 3) component: Int): Float
+    abstract fun getMaxValue(/*@IntRange(from = 0, to = 3)*/ component: Int): Float
 
     /**
      * Converts a color value from this color space's model to
@@ -255,7 +253,7 @@
      * @see toXyz
      * @see fromXyz
      */
-    @Size(3)
+    /*@Size(3)*/
     fun toXyz(r: Float, g: Float, b: Float): FloatArray {
         return toXyz(floatArrayOf(r, g, b))
     }
@@ -279,8 +277,8 @@
      * @see toXyz
      * @see fromXyz
      */
-    @Size(min = 3)
-    abstract fun toXyz(@Size(min = 3) v: FloatArray): FloatArray
+    /*@Size(min = 3)*/
+    abstract fun toXyz(/*@Size(min = 3)*/ v: FloatArray): FloatArray
 
     /**
      * Same as [toXyz], but returns only the x and y components packed into a long.
@@ -327,7 +325,7 @@
      * @see fromXyz
      * @see toXyz
      */
-    @Size(min = 3)
+    /*@Size(min = 3)*/
     fun fromXyz(x: Float, y: Float, z: Float): FloatArray {
         val xyz = FloatArray(model.componentCount)
         xyz[0] = x
@@ -355,8 +353,8 @@
      * @see fromXyz
      * @see toXyz
      */
-    @Size(min = 3)
-    abstract fun fromXyz(@Size(min = 3) v: FloatArray): FloatArray
+    /*@Size(min = 3)*/
+    abstract fun fromXyz(/*@Size(min = 3)*/ v: FloatArray): FloatArray
 
     /**
      * Returns a string representation of the object. This method returns
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpaces.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpaces.kt
index 510faf4..15c35b8 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpaces.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpaces.kt
@@ -18,8 +18,6 @@
 
 package androidx.compose.ui.graphics.colorspace
 
-import androidx.annotation.Size
-
 object ColorSpaces {
     internal val SrgbPrimaries = floatArrayOf(0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f)
     internal val Ntsc1953Primaries = floatArrayOf(0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f)
@@ -298,7 +296,7 @@
      * @return A non-null [ColorSpace] if a match is found, null otherwise
      */
     fun match(
-        @Size(9)
+        /*@Size(9)*/
         toXYZD50: FloatArray,
         function: TransferParameters
     ): ColorSpace? {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt
index e12d651..dfe2401 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.graphics.colorspace
 
-import androidx.annotation.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.util.unpackFloat1
 import androidx.compose.ui.util.unpackFloat2
@@ -131,7 +130,7 @@
      *
      * @see transform
      */
-    @Size(3)
+    /*@Size(3)*/
     fun transform(r: Float, g: Float, b: Float): FloatArray {
         return transform(floatArrayOf(r, g, b))
     }
@@ -147,8 +146,8 @@
      *
      * @see transform
      */
-    @Size(min = 3)
-    open fun transform(@Size(min = 3) v: FloatArray): FloatArray {
+    /*@Size(min = 3)*/
+    open fun transform(/*@Size(min = 3)*/ v: FloatArray): FloatArray {
         val xyz = transformSource.toXyz(v)
         if (transform != null) {
             xyz[0] *= transform[0]
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt
index d9577e4..fb50d9a 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.graphics.colorspace
 
-import androidx.annotation.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.util.packFloats
 import kotlin.math.abs
@@ -323,7 +322,7 @@
      *
      * @see whitePoint
      */
-    @Size(6)
+    /*@Size(6)*/
     fun getPrimaries(): FloatArray = primaries.copyOf()
 
     /**
@@ -340,7 +339,7 @@
      *
      * @see getInverseTransform
      */
-    @Size(9)
+    /*@Size(9)*/
     fun getTransform(): FloatArray = transform.copyOf()
 
     /**
@@ -357,7 +356,7 @@
      *
      * @see getTransform
      */
-    @Size(9)
+    /*@Size(9)*/
     fun getInverseTransform(): FloatArray = inverseTransform.copyOf()
 
     /**
@@ -379,8 +378,10 @@
      *  * The minimum valid value is >= the maximum valid value.
      */
     constructor(
-        @Size(min = 1) name: String,
-        @Size(9) toXYZ: FloatArray,
+        /*@Size(min = 1)*/
+        name: String,
+        /*@Size(9)*/
+        toXYZ: FloatArray,
         oetf: (Double) -> Double,
         eotf: (Double) -> Double
     ) : this(
@@ -430,8 +431,10 @@
      *  * The minimum valid value is >= the maximum valid value.
      */
     constructor(
-        @Size(min = 1) name: String,
-        @Size(min = 6, max = 9) primaries: FloatArray,
+        /*@Size(min = 1)*/
+        name: String,
+        /*@Size(min = 6, max = 9)*/
+        primaries: FloatArray,
         whitePoint: WhitePoint,
         oetf: (Double) -> Double,
         eotf: (Double) -> Double,
@@ -467,8 +470,10 @@
      *  * Gamma is negative.
      */
     constructor(
-        @Size(min = 1) name: String,
-        @Size(9) toXYZ: FloatArray,
+        /*@Size(min = 1)*/
+        name: String,
+        /*@Size(9)*/
+        toXYZ: FloatArray,
         function: TransferParameters
     ) : this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), function, MinId)
 
@@ -502,8 +507,10 @@
      *  * The transfer parameters are invalid.
      */
     constructor(
-        @Size(min = 1) name: String,
-        @Size(min = 6, max = 9) primaries: FloatArray,
+        /*@Size(min = 1)*/
+        name: String,
+        /*@Size(min = 6, max = 9)*/
+        primaries: FloatArray,
         whitePoint: WhitePoint,
         function: TransferParameters
     ) : this(name, primaries, whitePoint, function, MinId)
@@ -602,8 +609,10 @@
      * @see get
      */
     constructor(
-        @Size(min = 1) name: String,
-        @Size(9) toXYZ: FloatArray,
+        /*@Size(min = 1)*/
+        name: String,
+        /*@Size(9)*/
+        toXYZ: FloatArray,
         gamma: Double
     ) : this(
         name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), gamma, 0.0f, 1.0f,
@@ -642,8 +651,10 @@
      * @see get
      */
     constructor(
-        @Size(min = 1) name: String,
-        @Size(min = 6, max = 9) primaries: FloatArray,
+        /*@Size(min = 1)*/
+        name: String,
+        /*@Size(min = 6, max = 9)*/
+        primaries: FloatArray,
         whitePoint: WhitePoint,
         gamma: Double
     ) : this(name, primaries, whitePoint, gamma, 0.0f, 1.0f, MinId)
@@ -734,8 +745,8 @@
      *
      * @see getPrimaries
      */
-    @Size(min = 6)
-    fun getPrimaries(@Size(min = 6) primaries: FloatArray): FloatArray {
+    /*@Size(min = 6)*/
+    fun getPrimaries(/*@Size(min = 6)*/ primaries: FloatArray): FloatArray {
         return this.primaries.copyInto(primaries)
     }
 
@@ -756,8 +767,8 @@
      *
      * @see getInverseTransform
      */
-    @Size(min = 9)
-    fun getTransform(@Size(min = 9) transform: FloatArray): FloatArray {
+    /*@Size(min = 9)*/
+    fun getTransform(/*@Size(min = 9)*/ transform: FloatArray): FloatArray {
         return this.transform.copyInto(transform)
     }
 
@@ -779,8 +790,8 @@
      *
      * @see getTransform
      */
-    @Size(min = 9)
-    fun getInverseTransform(@Size(min = 9) inverseTransform: FloatArray): FloatArray {
+    /*@Size(min = 9)*/
+    fun getInverseTransform(/*@Size(min = 9)*/ inverseTransform: FloatArray): FloatArray {
         return this.inverseTransform.copyInto(inverseTransform)
     }
 
@@ -809,7 +820,7 @@
      * @see toLinear
      * @see fromLinear
      */
-    @Size(3)
+    /*@Size(3)*/
     fun toLinear(r: Float, g: Float, b: Float): FloatArray {
         return toLinear(floatArrayOf(r, g, b))
     }
@@ -831,8 +842,8 @@
      * @see toLinear
      * @see fromLinear
      */
-    @Size(min = 3)
-    fun toLinear(@Size(min = 3) v: FloatArray): FloatArray {
+    /*@Size(min = 3)*/
+    fun toLinear(/*@Size(min = 3)*/ v: FloatArray): FloatArray {
         v[0] = eotfFunc(v[0].toDouble()).toFloat()
         v[1] = eotfFunc(v[1].toDouble()).toFloat()
         v[2] = eotfFunc(v[2].toDouble()).toFloat()
@@ -856,7 +867,7 @@
      * @see fromLinear
      * @see toLinear
      */
-    @Size(3)
+    /*@Size(3)*/
     fun fromLinear(r: Float, g: Float, b: Float): FloatArray {
         return fromLinear(floatArrayOf(r, g, b))
     }
@@ -878,8 +889,8 @@
      * @see fromLinear
      * @see toLinear
      */
-    @Size(min = 3)
-    fun fromLinear(@Size(min = 3) v: FloatArray): FloatArray {
+    /*@Size(min = 3)*/
+    fun fromLinear(/*@Size(min = 3) */v: FloatArray): FloatArray {
         v[0] = oetfFunc(v[0].toDouble()).toFloat()
         v[1] = oetfFunc(v[1].toDouble()).toFloat()
         v[2] = oetfFunc(v[2].toDouble()).toFloat()
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/WhitePoint.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/WhitePoint.kt
index b6457ae..ce05ce7 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/WhitePoint.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/WhitePoint.kt
@@ -16,8 +16,6 @@
 
 package androidx.compose.ui.graphics.colorspace
 
-import androidx.annotation.Size
-
 /**
  * Class for constructing white points used in [RGB][Rgb] color space. The value is
  * stored in the CIE xyY color space. The Y component of the white point is assumed
@@ -40,7 +38,7 @@
      *
      * @return A new float array of length 3 containing XYZ values
      */
-    @Size(3)
+    /*@Size(3)*/
     internal fun toXyz(): FloatArray {
         return floatArrayOf(x / y, 1.0f, (1f - x - y) / y)
     }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
index c8ea35b..a34641d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.graphics.drawscope
 
-import androidx.annotation.FloatRange
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
@@ -105,7 +104,8 @@
         strokeWidth: Float,
         cap: StrokeCap,
         pathEffect: PathEffect?,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
     ) = drawParams.canvas.drawLine(
@@ -134,7 +134,8 @@
         strokeWidth: Float,
         cap: StrokeCap,
         pathEffect: PathEffect?,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
     ) = drawParams.canvas.drawLine(
@@ -160,7 +161,8 @@
         brush: Brush,
         topLeft: Offset,
         size: Size,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -179,7 +181,8 @@
         color: Color,
         topLeft: Offset,
         size: Size,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -197,7 +200,8 @@
     override fun drawImage(
         image: ImageBitmap,
         topLeft: Offset,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -226,7 +230,8 @@
         srcSize: IntSize,
         dstOffset: IntOffset,
         dstSize: IntSize,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -248,7 +253,8 @@
         srcSize: IntSize,
         dstOffset: IntOffset,
         dstSize: IntSize,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode,
@@ -270,7 +276,8 @@
         topLeft: Offset,
         size: Size,
         cornerRadius: CornerRadius,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -293,7 +300,8 @@
         size: Size,
         cornerRadius: CornerRadius,
         style: DrawStyle,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
     ) = drawParams.canvas.drawRoundRect(
@@ -313,7 +321,8 @@
         brush: Brush,
         radius: Float,
         center: Offset,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -330,7 +339,8 @@
         color: Color,
         radius: Float,
         center: Offset,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -347,7 +357,8 @@
         brush: Brush,
         topLeft: Offset,
         size: Size,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -366,7 +377,8 @@
         color: Color,
         topLeft: Offset,
         size: Size,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -388,7 +400,8 @@
         useCenter: Boolean,
         topLeft: Offset,
         size: Size,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -413,7 +426,8 @@
         useCenter: Boolean,
         topLeft: Offset,
         size: Size,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -434,7 +448,8 @@
     override fun drawPath(
         path: Path,
         color: Color,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -449,7 +464,8 @@
     override fun drawPath(
         path: Path,
         brush: Brush,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         style: DrawStyle,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -468,7 +484,8 @@
         strokeWidth: Float,
         cap: StrokeCap,
         pathEffect: PathEffect?,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
     ) = drawParams.canvas.drawPoints(
@@ -497,7 +514,8 @@
         strokeWidth: Float,
         cap: StrokeCap,
         pathEffect: PathEffect?,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
     ) = drawParams.canvas.drawPoints(
@@ -607,7 +625,8 @@
     private fun configurePaint(
         brush: Brush?,
         style: DrawStyle,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode,
         filterQuality: FilterQuality = DefaultFilterQuality
@@ -631,7 +650,8 @@
     private fun configurePaint(
         color: Color,
         style: DrawStyle,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode,
         filterQuality: FilterQuality = DefaultFilterQuality
@@ -653,7 +673,8 @@
         cap: StrokeCap,
         join: StrokeJoin,
         pathEffect: PathEffect?,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode,
         filterQuality: FilterQuality = DefaultFilterQuality
@@ -681,7 +702,8 @@
         cap: StrokeCap,
         join: StrokeJoin,
         pathEffect: PathEffect?,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode,
         filterQuality: FilterQuality = DefaultFilterQuality
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
index 2fa840f..7ef2dc3 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.graphics.drawscope
 
-import androidx.annotation.FloatRange
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
@@ -378,7 +377,8 @@
         strokeWidth: Float = Stroke.HairlineWidth,
         cap: StrokeCap = Stroke.DefaultCap,
         pathEffect: PathEffect? = null,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
     )
@@ -405,7 +405,8 @@
         strokeWidth: Float = Stroke.HairlineWidth,
         cap: StrokeCap = Stroke.DefaultCap,
         pathEffect: PathEffect? = null,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
     )
@@ -428,7 +429,8 @@
         brush: Brush,
         topLeft: Offset = Offset.Zero,
         size: Size = this.size.offsetSize(topLeft),
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -452,7 +454,8 @@
         color: Color,
         topLeft: Offset = Offset.Zero,
         size: Size = this.size.offsetSize(topLeft),
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -473,7 +476,8 @@
     fun drawImage(
         image: ImageBitmap,
         topLeft: Offset = Offset.Zero,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -517,7 +521,8 @@
         srcSize: IntSize = IntSize(image.width, image.height),
         dstOffset: IntOffset = IntOffset.Zero,
         dstSize: IntSize = srcSize,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -554,7 +559,8 @@
         srcSize: IntSize = IntSize(image.width, image.height),
         dstOffset: IntOffset = IntOffset.Zero,
         dstSize: IntSize = srcSize,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode,
@@ -593,7 +599,8 @@
         topLeft: Offset = Offset.Zero,
         size: Size = this.size.offsetSize(topLeft),
         cornerRadius: CornerRadius = CornerRadius.Zero,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -619,7 +626,8 @@
         size: Size = this.size.offsetSize(topLeft),
         cornerRadius: CornerRadius = CornerRadius.Zero,
         style: DrawStyle = Fill,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
     )
@@ -641,7 +649,8 @@
         brush: Brush,
         radius: Float = size.minDimension / 2.0f,
         center: Offset = this.center,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -664,7 +673,8 @@
         color: Color,
         radius: Float = size.minDimension / 2.0f,
         center: Offset = this.center,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -690,7 +700,8 @@
         brush: Brush,
         topLeft: Offset = Offset.Zero,
         size: Size = this.size.offsetSize(topLeft),
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -716,7 +727,8 @@
         color: Color,
         topLeft: Offset = Offset.Zero,
         size: Size = this.size.offsetSize(topLeft),
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -751,7 +763,8 @@
         useCenter: Boolean,
         topLeft: Offset = Offset.Zero,
         size: Size = this.size.offsetSize(topLeft),
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -786,7 +799,8 @@
         useCenter: Boolean,
         topLeft: Offset = Offset.Zero,
         size: Size = this.size.offsetSize(topLeft),
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -809,7 +823,8 @@
     fun drawPath(
         path: Path,
         color: Color,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -831,7 +846,8 @@
     fun drawPath(
         path: Path,
         brush: Brush,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         style: DrawStyle = Fill,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -860,7 +876,8 @@
         strokeWidth: Float = Stroke.HairlineWidth,
         cap: StrokeCap = StrokeCap.Butt,
         pathEffect: PathEffect? = null,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
     )
@@ -888,7 +905,8 @@
         strokeWidth: Float = Stroke.HairlineWidth,
         cap: StrokeCap = StrokeCap.Butt,
         pathEffect: PathEffect? = null,
-        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
     )
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
index 1d92b39..c7d3794 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
@@ -162,12 +162,27 @@
     density: Density
 ): Float {
     return when (lineHeight.type) {
-        TextUnitType.Sp -> with(density) { lineHeight.toPx() }
+        TextUnitType.Sp -> {
+            if (!isNonLinearFontScalingActive(density)) {
+                // Non-linear font scaling is not being used, this SP is safe to use directly.
+                with(density) { lineHeight.toPx() }
+            } else {
+                // Determine the intended line height multiplier and use that, since non-linear font
+                // scaling may compress the line height if it is much larger than the font size.
+                // i.e. preserve the original proportions rather than the absolute converted value.
+                val fontSizeSp = with(density) { contextFontSize.toSp() }
+                val lineHeightMultiplier = lineHeight.value / fontSizeSp.value
+                lineHeightMultiplier * contextFontSize
+            }
+        }
         TextUnitType.Em -> lineHeight.value * contextFontSize
         else -> Float.NaN
     }
 }
 
+// TODO(b/294384826): replace this with the actual platform method once available in core
+private fun isNonLinearFontScalingActive(density: Density) = density.fontScale > 1.05
+
 internal fun Spannable.setSpanStyles(
     contextTextStyle: TextStyle,
     spanStyles: List<AnnotatedString.Range<SpanStyle>>,
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index e2ac70a..5238584 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2681,6 +2681,12 @@
     method public long calculateRecommendedTimeoutMillis(long originalTimeoutMillis, optional boolean containsIcons, optional boolean containsText, optional boolean containsControls);
   }
 
+  public final class AndroidComposeViewAccessibilityDelegateCompat_androidKt {
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static boolean getDisableContentCapture();
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static void setDisableContentCapture(boolean);
+    property @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static final boolean DisableContentCapture;
+  }
+
   public final class AndroidCompositionLocals_androidKt {
     method public static androidx.compose.runtime.ProvidableCompositionLocal<android.content.res.Configuration> getLocalConfiguration();
     method public static androidx.compose.runtime.ProvidableCompositionLocal<android.content.Context> getLocalContext();
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 9866e4c..21e9e7a 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2734,6 +2734,12 @@
     method public long calculateRecommendedTimeoutMillis(long originalTimeoutMillis, optional boolean containsIcons, optional boolean containsText, optional boolean containsControls);
   }
 
+  public final class AndroidComposeViewAccessibilityDelegateCompat_androidKt {
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static boolean getDisableContentCapture();
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static void setDisableContentCapture(boolean);
+    property @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static final boolean DisableContentCapture;
+  }
+
   public final class AndroidCompositionLocals_androidKt {
     method public static androidx.compose.runtime.ProvidableCompositionLocal<android.content.res.Configuration> getLocalConfiguration();
     method public static androidx.compose.runtime.ProvidableCompositionLocal<android.content.Context> getLocalContext();
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index 0a41454..299ab79 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -48,6 +48,7 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.LocalLayoutDirection
@@ -820,6 +821,34 @@
             .assertHeightIsEqualTo(10.dp)
     }
 
+    @Test
+    fun testInvalidationInsideOnSizeChanged() {
+        var someState by mutableStateOf(1)
+        var drawCount = 0
+
+        rule.setContent {
+            Box(
+                Modifier
+                    .drawBehind {
+                        @Suppress("UNUSED_EXPRESSION")
+                        someState
+                        drawCount++
+                    }
+                    .onSizeChanged {
+                        // assert that draw hasn't happened yet
+                        assertEquals(0, drawCount)
+                        someState++
+                    }
+                    .size(10.dp)
+            )
+        }
+        rule.runOnIdle {
+            // assert that state invalidation inside of onSizeChanged
+            // doesn't schedule additional draw
+            assertEquals(1, drawCount)
+        }
+    }
+
     // captureToImage() requires API level 26
     @RequiresApi(Build.VERSION_CODES.O)
     private fun SemanticsNodeInteraction.captureToBitmap() = captureToImage().asAndroidBitmap()
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 ef354a4..9f469db 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
@@ -60,6 +60,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.referentialEqualityPolicy
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.SessionMutex
@@ -1184,6 +1185,7 @@
             invalidateLayers(root)
         }
         measureAndLayout()
+        Snapshot.sendApplyNotifications()
 
         isDrawingContent = true
         // we don't have to observe here because the root has a layer modifier
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 047c75e..ee90886 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -50,6 +50,9 @@
 import androidx.collection.ArrayMap
 import androidx.collection.ArraySet
 import androidx.collection.SparseArrayCompat
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.R
 import androidx.compose.ui.geometry.Offset
@@ -262,13 +265,14 @@
 
     /**
      * True if any content capture service enabled in the system.
-     *
-     * TODO(b/272068594): follow up on improving the performance and actually enabling the content
-     * capture feature in production later.
      */
+    @OptIn(ExperimentalComposeUiApi::class)
     private val isEnabledForContentCapture: Boolean
         get() {
-            return contentCaptureForceEnabledForTesting
+            if (DisableContentCapture) {
+                return false
+            }
+            return contentCaptureSession != null || contentCaptureForceEnabledForTesting
         }
 
     /**
@@ -3642,3 +3646,16 @@
  */
 internal fun AndroidViewsHandler.semanticsIdToView(id: Int): View? =
     layoutNodeToHolder.entries.firstOrNull { it.key.semanticsId == id }?.value
+
+/**
+ * A flag to force disable the content capture feature.
+ *
+ * If you find any issues with the new feature, flip this flag to true to confirm they are newly
+ * introduced then file a bug.
+ */
+@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:Suppress("GetterSetterNames")
+@get:ExperimentalComposeUiApi
+@set:ExperimentalComposeUiApi
+@ExperimentalComposeUiApi
+var DisableContentCapture: Boolean by mutableStateOf(false)
diff --git a/core/core-performance-play-services/api/1.0.0-beta01.txt b/core/core-performance-play-services/api/1.0.0-beta01.txt
new file mode 100644
index 0000000..7e46ef7
--- /dev/null
+++ b/core/core-performance-play-services/api/1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.core.performance.play.services {
+
+  public final class PlayServicesDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public PlayServicesDevicePerformance(android.content.Context context);
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-performance-play-services/api/res-1.0.0-beta01.txt b/core/core-performance-play-services/api/res-1.0.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/core-performance-play-services/api/res-1.0.0-beta01.txt
diff --git a/core/core-performance-play-services/api/restricted_1.0.0-beta01.txt b/core/core-performance-play-services/api/restricted_1.0.0-beta01.txt
new file mode 100644
index 0000000..7e46ef7
--- /dev/null
+++ b/core/core-performance-play-services/api/restricted_1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.core.performance.play.services {
+
+  public final class PlayServicesDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public PlayServicesDevicePerformance(android.content.Context context);
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-performance-testing/api/1.0.0-beta01.txt b/core/core-performance-testing/api/1.0.0-beta01.txt
new file mode 100644
index 0000000..6e565c7
--- /dev/null
+++ b/core/core-performance-testing/api/1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.core.performance.testing {
+
+  public final class FakeDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public FakeDevicePerformance(int mediaPerformanceClass);
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-performance-testing/api/res-1.0.0-beta01.txt b/core/core-performance-testing/api/res-1.0.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/core-performance-testing/api/res-1.0.0-beta01.txt
diff --git a/core/core-performance-testing/api/restricted_1.0.0-beta01.txt b/core/core-performance-testing/api/restricted_1.0.0-beta01.txt
new file mode 100644
index 0000000..6e565c7
--- /dev/null
+++ b/core/core-performance-testing/api/restricted_1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.core.performance.testing {
+
+  public final class FakeDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public FakeDevicePerformance(int mediaPerformanceClass);
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-performance/api/1.0.0-beta01.txt b/core/core-performance/api/1.0.0-beta01.txt
new file mode 100644
index 0000000..05ff76d2
--- /dev/null
+++ b/core/core-performance/api/1.0.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.core.performance {
+
+  public final class DefaultDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public DefaultDevicePerformance();
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
+    method public int getMediaPerformanceClass();
+    property public abstract int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-performance/api/res-1.0.0-beta01.txt b/core/core-performance/api/res-1.0.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/core-performance/api/res-1.0.0-beta01.txt
diff --git a/core/core-performance/api/restricted_1.0.0-beta01.txt b/core/core-performance/api/restricted_1.0.0-beta01.txt
new file mode 100644
index 0000000..05ff76d2
--- /dev/null
+++ b/core/core-performance/api/restricted_1.0.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.core.performance {
+
+  public final class DefaultDevicePerformance implements androidx.core.performance.DevicePerformance {
+    ctor public DefaultDevicePerformance();
+    method public int getMediaPerformanceClass();
+    property public int mediaPerformanceClass;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
+    method public int getMediaPerformanceClass();
+    property public abstract int mediaPerformanceClass;
+  }
+
+}
+
diff --git a/core/core-performance/samples/src/main/java/androidx/core/performance/samples/Usage.kt b/core/core-performance/samples/src/main/java/androidx/core/performance/samples/Usage.kt
index 5f5be1c..6a81b51 100644
--- a/core/core-performance/samples/src/main/java/androidx/core/performance/samples/Usage.kt
+++ b/core/core-performance/samples/src/main/java/androidx/core/performance/samples/Usage.kt
@@ -16,25 +16,37 @@
 
 package androidx.core.performance.samples
 
+import android.app.Activity
 import android.app.Application
 import android.os.Build
+import android.os.Bundle
 import androidx.annotation.Sampled
-import androidx.core.performance.DefaultDevicePerformance
 import androidx.core.performance.DevicePerformance
+import androidx.core.performance.play.services.PlayServicesDevicePerformance
 
 @Sampled
 fun usage() {
 
     class MyApplication : Application() {
-
-        private lateinit var devicePerformance: DevicePerformance
+        lateinit var devicePerformance: DevicePerformance
 
         override fun onCreate() {
             // use a DevicePerformance derived class
-            devicePerformance = DefaultDevicePerformance()
+            devicePerformance = PlayServicesDevicePerformance(applicationContext)
+        }
+    }
+
+    class MyActivity : Activity() {
+        private lateinit var devicePerformance: DevicePerformance
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            // Production applications should use a dependency framework.
+            // See https://developer.android.com/training/dependency-injection for more information.
+            devicePerformance = (application as MyApplication).devicePerformance
         }
 
-        fun doSomeThing() {
+        override fun onResume() {
+            super.onResume()
             when {
                 devicePerformance.mediaPerformanceClass >= Build.VERSION_CODES.TIRAMISU -> {
                     // Provide the most premium experience for highest performing devices
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index f56473d..027bd5a 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -720,3 +720,5 @@
 w: file://\$SUPPORT/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
 # b/271306193 remove after aosp/2589888 :emoji:emoji:spdxSbomForRelease
 spdx sboms require a version but project: noto\-emoji\-compat\-flatbuffers has no specified version
+# > Configure project :internal-testutils-appcompat
+WARNING: The option setting 'android\.experimental\.lint\.reservedMemoryPerTask=[0-9]+g' is experimental\.
\ No newline at end of file
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 1c2c888..b6b1688 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -32,10 +32,10 @@
     docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01")
     docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
     docs("androidx.autofill:autofill:1.3.0-alpha01")
-    docs("androidx.benchmark:benchmark-common:1.2.0-beta04")
-    docs("androidx.benchmark:benchmark-junit4:1.2.0-beta04")
-    docs("androidx.benchmark:benchmark-macro:1.2.0-beta04")
-    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-beta04")
+    docs("androidx.benchmark:benchmark-common:1.2.0-beta05")
+    docs("androidx.benchmark:benchmark-junit4:1.2.0-beta05")
+    docs("androidx.benchmark:benchmark-macro:1.2.0-beta05")
+    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-beta05")
     docs("androidx.biometric:biometric:1.2.0-alpha05")
     docs("androidx.biometric:biometric-ktx:1.2.0-alpha05")
     samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha05")
@@ -358,10 +358,10 @@
     docsWithoutApiSince("androidx.textclassifier:textclassifier:1.0.0-alpha04")
     docs("androidx.tracing:tracing:1.3.0-alpha02")
     docs("androidx.tracing:tracing-ktx:1.3.0-alpha02")
-    docs("androidx.tracing:tracing-perfetto:1.0.0-beta02")
+    docs("androidx.tracing:tracing-perfetto:1.0.0-beta03")
     // TODO(243405142) clean-up
     docsWithoutApiSince("androidx.tracing:tracing-perfetto-common:1.0.0-alpha16")
-    docs("androidx.tracing:tracing-perfetto-handshake:1.0.0-beta02")
+    docs("androidx.tracing:tracing-perfetto-handshake:1.0.0-beta03")
     docs("androidx.transition:transition:1.5.0-alpha01")
     docs("androidx.transition:transition-ktx:1.5.0-alpha01")
     docs("androidx.tv:tv-foundation:1.0.0-alpha08")
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
index ab3b7d6..7ca4c07 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
@@ -32,6 +32,7 @@
 import androidx.core.view.ViewCompat
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
+import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.ViewModelStore
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -725,6 +726,83 @@
         assertThat(fragment1.requireView().parent).isNotNull()
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun replaceOperationWithAnimatorsThenCancel() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        val fragment1 = AnimatorFragment(R.layout.scene1)
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val fragment2 = AnimatorFragment()
+
+        fm1.beginTransaction()
+            .setCustomAnimations(
+                android.R.animator.fade_in,
+                android.R.animator.fade_out,
+                android.R.animator.fade_in,
+                android.R.animator.fade_out
+            )
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(fragment2.wasStarted).isTrue()
+        // We need to wait for the exit animation to end
+        assertThat(fragment1.endLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment2.resumeLatch = CountDownLatch(1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        if (FragmentManager.USE_PREDICTIVE_BACK) {
+            assertThat(fragment1.startLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+            assertThat(fragment2.startLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+            assertThat(fragment1.inProgress).isTrue()
+            assertThat(fragment2.inProgress).isTrue()
+        } else {
+            assertThat(fragment1.inProgress).isFalse()
+            assertThat(fragment1.inProgress).isFalse()
+        }
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackCancelled()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(fragment2.wasStarted).isTrue()
+        // Now fragment1 should be animating away
+        assertThat(fragment1.isAdded).isFalse()
+
+        // Now fragment2 should be animating back in
+        assertThat(fragment2.isAdded).isTrue()
+        assertThat(fm1.findFragmentByTag("2")).isEqualTo(fragment2)
+
+        assertThat(fragment2.resumeLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment2.view?.parent).isNotNull()
+
+        assertThat(fragment1.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+        assertThat(fragment1.view).isNull()
+    }
+
     private fun assertEnterPopExit(fragment: AnimatorFragment) {
         assertFragmentAnimation(fragment, 1, true, ENTER)
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
index a35f0c1..c90e41e 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
@@ -220,17 +220,10 @@
             operation2.addCompletionListener {
                 awaitingChanges = operation2.isAwaitingContainerChanges
             }
+            val operation = controller.operationsToExecute[0]
             onActivity {
-                fragmentStateManager1.moveToExpectedState()
-                fragmentStateManager2.moveToExpectedState()
-                // However, executePendingOperations(), since we're using our
-                // TestSpecialEffectsController, does immediately call complete()
-                // which in turn calls moveToExpectedState()
-                controller.executePendingOperations()
+                operation.complete()
             }
-            // Assert that we actually moved to the STARTED state
-            assertThat(fragment1.lifecycle.currentState)
-                .isEqualTo(Lifecycle.State.STARTED)
             assertThat(awaitingChanges).isTrue()
         }
     }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
index 6240244..c0ed273 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
@@ -594,7 +594,7 @@
                     if (isHideOperation) {
                         // Specifically for hide operations with Animator, we can't
                         // applyState until the Animator finishes
-                        operation.finalState.applyState(viewToAnimate)
+                        operation.finalState.applyState(viewToAnimate, container)
                     }
                     animatorInfo.operation.completeEffect(this@AnimatorEffect)
                     if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 0718eba..3a1220f 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -2922,7 +2922,9 @@
                 mHost.getHandler().post(new Runnable() {
                     @Override
                     public void run() {
-                        controller.executePendingOperations();
+                        if (controller.isPendingExecute()) {
+                            controller.executePendingOperations();
+                        }
                     }
                 });
             } else {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
index 8a67e9c..44abd50 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
@@ -37,6 +37,8 @@
     private val runningOperations = mutableListOf<Operation>()
     private var operationDirectionIsPop = false
     private var isContainerPostponed = false
+    // We have a call to executePendingTransactions in a handler
+    private var pendingExecute = false
 
     /**
      * Checks what [lifecycle impact][Operation.LifecycleImpact] of special effect for the
@@ -149,7 +151,7 @@
             // Ensure that we still run the applyState() call for pending operations
             operation.addCompletionListener {
                 if (pendingOperations.contains(operation)) {
-                    operation.finalState.applyState(operation.fragment.mView)
+                    operation.finalState.applyState(operation.fragment.mView, container)
                 }
             }
             // Ensure that we remove the Operation from the list of
@@ -178,6 +180,11 @@
             // associated with the last entering Operation is postponed
             isContainerPostponed = lastEnteringFragment?.isPostponed ?: false
         }
+        pendingExecute = true
+    }
+
+    fun isPendingExecute(): Boolean {
+        return pendingExecute
     }
 
     fun forcePostponedExecutePendingOperations() {
@@ -198,6 +205,7 @@
             // No operations should execute while the container is postponed
             return
         }
+        pendingExecute = false
         // If the container is not attached to the window, ignore the special effect
         // since none of the special effect systems will run them anyway.
         if (!ViewCompat.isAttachedToWindow(container)) {
@@ -206,7 +214,29 @@
             return
         }
         synchronized(pendingOperations) {
-            if (pendingOperations.isNotEmpty()) {
+            if (pendingOperations.isEmpty()) {
+                val currentlyRunningOperations = runningOperations.toMutableList()
+                runningOperations.clear()
+                for (operation in currentlyRunningOperations) {
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(
+                            FragmentManager.TAG,
+                            "SpecialEffectsController: Cancelling operation $operation " +
+                                "with no incoming pendingOperations"
+                        )
+                    }
+                    // Cancel the currently running operation immediately as this is the case
+                    // where we got an handleOnBackCanceled callback and we don't want to run
+                    // any effects back to start cause they will have already been seeked to
+                    // start
+                    operation.cancel(container, false)
+                    if (!operation.isComplete) {
+                        // Re-add any animations that didn't synchronously call complete()
+                        // to continue to track them as running operations
+                        runningOperations.add(operation)
+                    }
+                }
+            } else {
                 val currentlyRunningOperations = runningOperations.toMutableList()
                 runningOperations.clear()
                 for (operation in currentlyRunningOperations) {
@@ -216,7 +246,9 @@
                             "SpecialEffectsController: Cancelling operation $operation"
                         )
                     }
-                    // Cancel with seeking if the fragment is transitioning
+                    // Cancel with seeking if the fragment is transitioning as this is the case
+                    // where another operation is about to run while we are still seeking
+                    // so we should move our current effect back to the start.
                     operation.cancel(container, operation.fragment.mTransitioning)
                     if (!operation.isComplete) {
                         // Re-add any animations that didn't synchronously call complete()
@@ -275,7 +307,7 @@
 
     internal fun applyContainerChangesToOperation(operation: Operation) {
         if (operation.isAwaitingContainerChanges) {
-            operation.finalState.applyState(operation.fragment.requireView())
+            operation.finalState.applyState(operation.fragment.requireView(), container)
             operation.isAwaitingContainerChanges = false
         }
     }
@@ -473,8 +505,9 @@
              * Applies this state to the given View.
              *
              * @param view The View to apply this state to.
+             * @param container The ViewGroup to add the view too if it does not have a parent.
              */
-            fun applyState(view: View) {
+            fun applyState(view: View, container: ViewGroup) {
                 when (this) {
                     REMOVED -> {
                         val parent = view.parent as? ViewGroup
@@ -496,6 +529,16 @@
                                     "Setting view $view to VISIBLE"
                             )
                         }
+                        val parent = view.parent as? ViewGroup
+                        if (parent == null) {
+                            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                                Log.v(
+                                    FragmentManager.TAG, "SpecialEffectsController: " +
+                                        "Adding view $view to Container $container"
+                                )
+                            }
+                            container.addView(view)
+                        }
                         view.visibility = View.VISIBLE
                     }
 
@@ -633,6 +676,7 @@
                     // moves it back to ADDING
                     this.finalState = State.VISIBLE
                     this.lifecycleImpact = LifecycleImpact.ADDING
+                    this.isAwaitingContainerChanges = true
                 }
                 LifecycleImpact.REMOVING -> {
                     if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
@@ -646,6 +690,7 @@
                     // Any REMOVING operation overrides whatever we had before
                     this.finalState = State.REMOVED
                     this.lifecycleImpact = LifecycleImpact.REMOVING
+                    this.isAwaitingContainerChanges = true
                 }
                 LifecycleImpact.NONE -> // This is a hide or show operation
                     if (this.finalState != State.REMOVED) {
diff --git a/gradle.properties b/gradle.properties
index bec21dd..b1e0c9f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -31,6 +31,7 @@
 # Pending cleanup to support non-constant R class IDs b/260409846
 android.nonFinalResIds=false
 android.experimental.lint.missingBaselineIsEmptyBaseline=true
+android.experimental.lint.reservedMemoryPerTask=1g
 
 # Do generate versioned API files
 androidx.writeVersionedApiFiles=true
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index 37f2bce..7c7872e 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -55,8 +55,8 @@
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
-    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T param);
-    method @WorkerThread public void onDrawMultiBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
+    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int width, int height, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T param);
+    method @WorkerThread public void onDrawMultiBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int width, int height, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
     method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
     method @WorkerThread public default void onMultiBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
   }
@@ -117,7 +117,7 @@
 
   public static interface GLFrameBufferRenderer.Callback {
     method @WorkerThread public default void onDrawComplete(androidx.graphics.surface.SurfaceControlCompat targetSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction, androidx.graphics.opengl.FrameBuffer frameBuffer, androidx.hardware.SyncFenceCompat? syncFence);
-    method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform);
+    method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager, int width, int height, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform);
   }
 
   public final class GLRenderer {
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index fb63328..2d57c61 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -55,8 +55,8 @@
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
-    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T param);
-    method @WorkerThread public void onDrawMultiBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
+    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int width, int height, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T param);
+    method @WorkerThread public void onDrawMultiBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int width, int height, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
     method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
     method @WorkerThread public default void onMultiBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
   }
@@ -117,7 +117,7 @@
 
   public static interface GLFrameBufferRenderer.Callback {
     method @WorkerThread public default void onDrawComplete(androidx.graphics.surface.SurfaceControlCompat targetSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction, androidx.graphics.opengl.FrameBuffer frameBuffer, androidx.hardware.SyncFenceCompat? syncFence);
-    method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform);
+    method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager, int width, int height, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform);
   }
 
   public final class GLRenderer {
diff --git a/graphics/graphics-core/samples/src/main/java/androidx/graphics/core/samples/GLFrameBufferRendererSample.kt b/graphics/graphics-core/samples/src/main/java/androidx/graphics/core/samples/GLFrameBufferRendererSample.kt
index 47fbf1d..cfada23 100644
--- a/graphics/graphics-core/samples/src/main/java/androidx/graphics/core/samples/GLFrameBufferRendererSample.kt
+++ b/graphics/graphics-core/samples/src/main/java/androidx/graphics/core/samples/GLFrameBufferRendererSample.kt
@@ -35,6 +35,8 @@
 
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index f54aac7..72cd3e3 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -73,6 +73,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -94,6 +96,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -175,6 +179,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -196,6 +202,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -301,6 +309,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Int
@@ -310,6 +320,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Int>
@@ -425,6 +437,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Int
@@ -469,6 +483,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Int>
@@ -521,6 +537,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Int
@@ -530,6 +548,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Int>
@@ -621,6 +641,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Int
@@ -642,6 +664,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Int>
@@ -727,6 +751,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Int
@@ -736,6 +762,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Int>
@@ -819,6 +847,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -845,6 +875,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -898,6 +930,8 @@
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -925,6 +959,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -1023,6 +1059,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -1044,6 +1082,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -1127,14 +1167,18 @@
         val secondFrontBufferLatch = CountDownLatch(1)
         var bufferTransform = BufferTransformHintResolver.UNKNOWN_TRANSFORM
         var surfaceView: SurfaceView? = null
+        var surfaceWidth = 0
+        var surfaceHeight = 0
         val surfaceHolderCallbacks = object : SurfaceHolder.Callback {
             override fun surfaceCreated(p0: SurfaceHolder) {
                 // NO-OP
             }
 
-            override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
+            override fun surfaceChanged(p0: SurfaceHolder, format: Int, width: Int, height: Int) {
                 bufferTransform =
                     BufferTransformHintResolver().getBufferTransformHint(surfaceView!!)
+                surfaceWidth = width
+                surfaceHeight = height
             }
 
             override fun surfaceDestroyed(p0: SurfaceHolder) {
@@ -1152,10 +1196,14 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
             ) {
+                assertEquals(surfaceWidth, width)
+                assertEquals(surfaceHeight, height)
                 GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
@@ -1173,10 +1221,14 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
             ) {
+                assertEquals(surfaceWidth, width)
+                assertEquals(surfaceHeight, height)
                 GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
@@ -1244,6 +1296,8 @@
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -1253,6 +1307,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -1299,6 +1355,8 @@
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -1308,6 +1366,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -1374,6 +1434,8 @@
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -1383,6 +1445,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -1407,6 +1471,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -1416,6 +1482,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -1456,6 +1524,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -1480,6 +1550,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
@@ -1669,6 +1741,8 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
@@ -1678,6 +1752,8 @@
 
             override fun onDrawMultiBufferedLayer(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
index 3392982..7dd500f 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
@@ -62,6 +62,8 @@
 
         override fun onDrawFrontBufferedLayer(
             eglManager: EGLManager,
+            width: Int,
+            height: Int,
             bufferInfo: BufferInfo,
             transform: FloatArray,
             param: FloatArray
@@ -92,6 +94,8 @@
 
         override fun onDrawMultiBufferedLayer(
             eglManager: EGLManager,
+            width: Int,
+            height: Int,
             bufferInfo: BufferInfo,
             transform: FloatArray,
             params: Collection<FloatArray>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferView.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferView.kt
index 3e720ada..a4c018d 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferView.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferView.kt
@@ -61,6 +61,8 @@
 
         override fun onDrawFrame(
             eglManager: EGLManager,
+            width: Int,
+            height: Int,
             bufferInfo: BufferInfo,
             transform: FloatArray
         ) {
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLFrameBufferRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLFrameBufferRendererTest.kt
index ac06484..84e3b6e 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLFrameBufferRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLFrameBufferRendererTest.kt
@@ -21,6 +21,7 @@
 import android.opengl.GLES20
 import android.opengl.Matrix
 import android.os.Build
+import android.view.SurfaceHolder
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
 import androidx.graphics.lowlatency.BufferInfo
@@ -68,6 +69,8 @@
         val callbacks = object : GLFrameBufferRenderer.Callback {
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
@@ -120,6 +123,8 @@
         val callbacks = object : GLFrameBufferRenderer.Callback {
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
@@ -156,6 +161,8 @@
         val callbacks = object : GLFrameBufferRenderer.Callback {
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
@@ -205,6 +212,8 @@
         val callbacks = object : GLFrameBufferRenderer.Callback {
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
@@ -253,6 +262,8 @@
         val callbacks = object : GLFrameBufferRenderer.Callback {
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
@@ -307,6 +318,8 @@
 
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray,
             ) {
@@ -339,6 +352,8 @@
     @Test
     fun testRenderFrameBuffer() {
         val renderLatch = CountDownLatch(1)
+        var surfaceWidth = 0
+        var surfaceHeight = 0
         val callbacks = object : GLFrameBufferRenderer.Callback {
 
             val mProjectionMatrix = FloatArray(16)
@@ -346,9 +361,13 @@
 
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
+                assertEquals(surfaceWidth, width)
+                assertEquals(surfaceHeight, height)
                 GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
@@ -386,11 +405,32 @@
         }
         var renderer: GLFrameBufferRenderer? = null
         var surfaceView: SurfaceView? = null
+
         try {
             val scenario = ActivityScenario.launch(SurfaceViewTestActivity::class.java)
                 .moveToState(Lifecycle.State.CREATED)
                 .onActivity {
-                    surfaceView = it.getSurfaceView()
+                    surfaceView = it.getSurfaceView().apply {
+                        holder.addCallback(object : SurfaceHolder.Callback {
+                            override fun surfaceCreated(holder: SurfaceHolder) {
+                                // no-op
+                            }
+
+                            override fun surfaceChanged(
+                                holder: SurfaceHolder,
+                                format: Int,
+                                width: Int,
+                                height: Int
+                            ) {
+                                surfaceWidth = width
+                                surfaceHeight = height
+                            }
+
+                            override fun surfaceDestroyed(holder: SurfaceHolder) {
+                                // no-op
+                            }
+                        })
+                    }
                     renderer = GLFrameBufferRenderer.Builder(surfaceView!!, callbacks).build()
                 }
 
@@ -425,6 +465,8 @@
 
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
@@ -463,6 +505,8 @@
         val callback = object : GLFrameBufferRenderer.Callback {
             override fun onDrawFrame(
                 eglManager: EGLManager,
+                width: Int,
+                height: Int,
                 bufferInfo: BufferInfo,
                 transform: FloatArray
             ) {
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
index d9a5fb1..5a8b912 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
@@ -34,6 +34,11 @@
     val transform: FloatArray
         get() = mViewTransform
 
+    var logicalWidth = 0
+        private set
+
+    var logicalHeight = 0
+        private set
     var glWidth = 0
         private set
 
@@ -66,6 +71,8 @@
     ) {
         val fWidth = width.toFloat()
         val fHeight = height.toFloat()
+        logicalWidth = width
+        logicalHeight = height
         glWidth = width
         glHeight = height
         computedTransform = transformHint
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index 39184c64..181ccd4 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -89,6 +89,8 @@
     private val mFrontBufferedCallbacks = object : GLFrameBufferRenderer.Callback {
         override fun onDrawFrame(
             eglManager: EGLManager,
+            width: Int,
+            height: Int,
             bufferInfo: BufferInfo,
             transform: FloatArray
         ) {
@@ -101,6 +103,8 @@
             mActiveSegment.next { param ->
                 callback.onDrawFrontBufferedLayer(
                     eglManager,
+                    width,
+                    height,
                     bufferInfo,
                     transform,
                     param
@@ -172,6 +176,8 @@
     private val mMultiBufferedRenderCallbacks = object : GLFrameBufferRenderer.Callback {
         override fun onDrawFrame(
             eglManager: EGLManager,
+            width: Int,
+            height: Int,
             bufferInfo: BufferInfo,
             transform: FloatArray
         ) {
@@ -180,6 +186,8 @@
             GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
             callback.onDrawMultiBufferedLayer(
                 eglManager,
+                width,
+                height,
                 bufferInfo,
                 transform,
                 mSegments.poll() ?: Collections.emptyList()
@@ -681,6 +689,10 @@
          * parameters.
          * @param eglManager [EGLManager] useful in configuring EGL objects to be used when issuing
          * OpenGL commands to render into the front buffered layer
+         * @param width Logical width of the content to render. This dimension matches what is
+         * provided from [SurfaceHolder.Callback.surfaceChanged]
+         * @param height Logical height of the content to render. This dimension matches what is
+         * provided from [SurfaceHolder.Callback.surfaceChanged]
          * @param bufferInfo [BufferInfo] about the buffer that is being rendered into. This
          * includes the width and height of the buffer which can be different than the corresponding
          * dimensions of the [SurfaceView] provided to the [GLFrontBufferedRenderer] as pre-rotation
@@ -719,6 +731,8 @@
         @WorkerThread
         fun onDrawFrontBufferedLayer(
             eglManager: EGLManager,
+            width: Int,
+            height: Int,
             bufferInfo: BufferInfo,
             transform: FloatArray,
             param: T
@@ -729,6 +743,10 @@
          * parameters.
          * @param eglManager [EGLManager] useful in configuring EGL objects to be used when issuing
          * OpenGL commands to render into the multi buffered layer
+         * @param width Logical width of the content to render. This dimension matches what is
+         * provided from [SurfaceHolder.Callback.surfaceChanged]
+         * @param height Logical height of the content to render. This dimension matches what is
+         * provided from [SurfaceHolder.Callback.surfaceChanged]
          * @param bufferInfo [BufferInfo] about the buffer that is being rendered into. This
          * includes the width and height of the buffer which can be different than the corresponding
          * dimensions of the [SurfaceView] provided to the [GLFrontBufferedRenderer] as pre-rotation
@@ -792,6 +810,8 @@
         @WorkerThread
         fun onDrawMultiBufferedLayer(
             eglManager: EGLManager,
+            width: Int,
+            height: Int,
             bufferInfo: BufferInfo,
             transform: FloatArray,
             params: Collection<T>
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLFrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLFrameBufferRenderer.kt
index 14277ec..4b08f54 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLFrameBufferRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLFrameBufferRenderer.kt
@@ -91,10 +91,10 @@
          * instance as the [GLFrameBufferRenderer] will consume but not release it.
          *
          * @param parentSurfaceControl Parent [SurfaceControlCompat] instance.
-         * @param width Logical width of the buffers to be rendered into. This would correspond to
-         * the width of a [android.view.View].
-         * @param height Logical height of the buffers to be rendered into. This would correspond to
-         * the height of a [android.view.View].
+         * @param width Logical width of the content to render. This dimension matches what is
+         * provided from [SurfaceHolder.Callback.surfaceChanged].
+         * @param height Logical height of the content to render. This dimension matches what is
+         * provided from [SurfaceHolder.Callback.surfaceChanged].
          * @param transformHint Hint used to specify how to pre-rotate content to optimize
          * consumption of content by the display without having to introduce an additional GPU pass
          * to handle rotation.
@@ -369,6 +369,9 @@
     ): FrameBufferRenderer = FrameBufferRenderer(
         object : FrameBufferRenderer.RenderCallback {
 
+            private val width = bufferTransformer.logicalWidth
+            private val height = bufferTransformer.logicalHeight
+
             private val bufferInfo = BufferInfo().apply {
                 this.width = bufferTransformer.glWidth
                 this.height = bufferTransformer.glHeight
@@ -390,7 +393,13 @@
             override fun onDraw(eglManager: EGLManager) {
                 val buffer = mCurrentFrameBuffer
                 if (buffer != null && !buffer.isClosed) {
-                    callback.onDrawFrame(eglManager, bufferInfo, bufferTransformer.transform)
+                    callback.onDrawFrame(
+                        eglManager,
+                        width,
+                        height,
+                        bufferInfo,
+                        bufferTransformer.transform
+                    )
                 }
             }
 
@@ -531,6 +540,10 @@
          * buffer with the specified parameters.
          * @param eglManager [EGLManager] useful in configuring EGL objects to be used when issuing
          * OpenGL commands to render into the front buffered layer
+         * @param width Logical width of the content to render. This dimension matches what is
+         * provided from [SurfaceHolder.Callback.surfaceChanged]
+         * @param height Logical height of the content to render. This dimension matches what is
+         * provided from [SurfaceHolder.Callback.surfaceChanged]
          * @param bufferInfo [BufferInfo] about the buffer that is being rendered into. This
          * includes the width and height of the buffer which can be different than the corresponding
          * dimensions of the [SurfaceView] provided to the [GLFrameBufferRenderer] as pre-rotation
@@ -568,6 +581,8 @@
         @WorkerThread
         fun onDrawFrame(
             eglManager: EGLManager,
+            width: Int,
+            height: Int,
             bufferInfo: BufferInfo,
             transform: FloatArray
         )
diff --git a/graphics/graphics-shapes/api/current.txt b/graphics/graphics-shapes/api/current.txt
index b2e61e0..ffd01a3 100644
--- a/graphics/graphics-shapes/api/current.txt
+++ b/graphics/graphics-shapes/api/current.txt
@@ -1,6 +1,13 @@
 // Signature format: 4.0
 package androidx.graphics.shapes {
 
+  public final class AndroidKt {
+    method public static android.graphics.Path toPath(androidx.graphics.shapes.Morph, float progress, optional android.graphics.Path path);
+    method public static android.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon);
+    method public static android.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon, optional android.graphics.Path path);
+    method public static androidx.graphics.shapes.RoundedPolygon transformed(androidx.graphics.shapes.RoundedPolygon, android.graphics.Matrix matrix);
+  }
+
   public final class CornerRounding {
     ctor public CornerRounding(optional @FloatRange(from=0.0) float radius, optional @FloatRange(from=0.0, to=1.0) float smoothing);
     method public float getRadius();
diff --git a/graphics/graphics-shapes/api/restricted_current.txt b/graphics/graphics-shapes/api/restricted_current.txt
index b2e61e0..ffd01a3 100644
--- a/graphics/graphics-shapes/api/restricted_current.txt
+++ b/graphics/graphics-shapes/api/restricted_current.txt
@@ -1,6 +1,13 @@
 // Signature format: 4.0
 package androidx.graphics.shapes {
 
+  public final class AndroidKt {
+    method public static android.graphics.Path toPath(androidx.graphics.shapes.Morph, float progress, optional android.graphics.Path path);
+    method public static android.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon);
+    method public static android.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon, optional android.graphics.Path path);
+    method public static androidx.graphics.shapes.RoundedPolygon transformed(androidx.graphics.shapes.RoundedPolygon, android.graphics.Matrix matrix);
+  }
+
   public final class CornerRounding {
     ctor public CornerRounding(optional @FloatRange(from=0.0) float radius, optional @FloatRange(from=0.0, to=1.0) float smoothing);
     method public float getRadius();
diff --git a/graphics/graphics-shapes/src/androidMain/kotlin/androidx/graphics/shapes/Android.kt b/graphics/graphics-shapes/src/androidMain/kotlin/androidx/graphics/shapes/Android.kt
new file mode 100644
index 0000000..7320379
--- /dev/null
+++ b/graphics/graphics-shapes/src/androidMain/kotlin/androidx/graphics/shapes/Android.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.graphics.shapes
+
+import android.graphics.Matrix
+import android.graphics.Path
+
+/**
+ * Transforms a [RoundedPolygon] by the given matrix.
+ *
+ * @param matrix The matrix by which the polygon is to be transformed
+ */
+fun RoundedPolygon.transformed(matrix: Matrix):
+    RoundedPolygon = transformed {
+        val tempArray = FloatArray(2)
+        // TODO: Should we have a fast path for when the MutablePoint is array-backed?
+        tempArray[0] = x
+        tempArray[1] = y
+        matrix.mapPoints(tempArray)
+        x = tempArray[0]
+        y = tempArray[1]
+    }
+
+/**
+ * Gets a [Path] representation for a [RoundedPolygon] shape.
+ *
+ * @param path an optional [Path] object which, if supplied, will avoid the function having
+ * to create a new [Path] object
+ */
+@JvmOverloads
+fun RoundedPolygon.toPath(path: Path = Path()): Path {
+    pathFromCubics(path, cubics)
+    return path
+}
+
+fun Morph.toPath(progress: Float, path: Path = Path()): Path {
+    pathFromCubics(path, asCubics(progress))
+    return path
+}
+
+private fun pathFromCubics(
+    path: Path,
+    cubics: List<Cubic>
+) {
+    var first = true
+    path.rewind()
+    for (i in 0 until cubics.size) {
+        val cubic = cubics[i]
+        if (first) {
+            path.moveTo(cubic.anchor0X, cubic.anchor0Y)
+            first = false
+        }
+        path.cubicTo(
+            cubic.control0X, cubic.control0Y,
+            cubic.control1X, cubic.control1Y,
+            cubic.anchor1X, cubic.anchor1Y
+        )
+    }
+    path.close()
+}
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/Android.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/Android.kt
deleted file mode 100644
index ff68f78..0000000
--- a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/Android.kt
+++ /dev/null
@@ -1,62 +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.graphics.shapes.test
-
-import android.graphics.Canvas
-import android.graphics.Matrix
-import android.graphics.Paint
-import android.graphics.Path
-import androidx.core.graphics.scaleMatrix
-import androidx.graphics.shapes.Cubic
-import androidx.graphics.shapes.RoundedPolygon
-
-fun RoundedPolygon.transformed(matrix: Matrix, tmp: FloatArray = FloatArray(2)):
-    RoundedPolygon = transformed {
-        // TODO: Should we have a fast path for when the MutablePoint is array-backed?
-        tmp[0] = x
-        tmp[1] = y
-        matrix.mapPoints(tmp)
-        x = tmp[0]
-        x = tmp[1]
-    }
-
-/**
- * Function used to create a Path from this CubicShape.
- * This usually should only be called once and cached, since CubicShape is immutable.
- */
-fun Iterator<Cubic>.toPath(path: Path = Path()): Path {
-    path.rewind()
-    var first = true
-    for (bezier in this) {
-        if (first) {
-            path.moveTo(bezier.anchor0X, bezier.anchor0Y)
-            first = false
-        }
-        path.cubicTo(
-            bezier.control0X, bezier.control0Y,
-            bezier.control1X, bezier.control1Y,
-            bezier.anchor1X, bezier.anchor1Y
-        )
-    }
-    path.close()
-    return path
-}
-
-fun Canvas.drawPolygon(shape: RoundedPolygon, scale: Int, paint: Paint) =
-    drawPath(shape.cubics.iterator().toPath().apply {
-        transform(scaleMatrix(scale.toFloat(), scale.toFloat()))
-}, paint)
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/MorphView.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/MorphView.kt
new file mode 100644
index 0000000..4c226b0
--- /dev/null
+++ b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/MorphView.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.graphics.shapes.test
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.RectF
+import android.view.View
+import android.view.animation.OvershootInterpolator
+import androidx.graphics.shapes.Morph
+import androidx.graphics.shapes.toPath
+import kotlin.math.max
+import kotlin.math.min
+
+class MorphView(context: Context, morph: Morph) : View(context) {
+    val paint = Paint()
+    val path = Path()
+    private var pathBounds = RectF()
+    val overshooter = OvershootInterpolator()
+
+    var morph = morph
+        set(value) {
+            field = value
+            val animator = ObjectAnimator.ofFloat(this, "progress", 0f, 1f)
+            animator.duration = 500
+            animator.interpolator = overshooter
+            animator.start()
+        }
+
+    var progress: Float = 0f
+        set(value) {
+            field = value
+            setupPath(value)
+            invalidate()
+        }
+
+    init {
+        paint.setColor(Color.WHITE)
+    }
+
+    private fun setupPath(progress: Float) {
+        morph.toPath(progress, path)
+        path.computeBounds(pathBounds, false)
+        val viewportSize = min(width, height).toFloat()
+        val pathSize = max(pathBounds.width(), pathBounds.height())
+        val scaleFactor = viewportSize / pathSize
+        val pathCenterX = pathBounds.left + pathBounds.width() / 2
+        val pathCenterY = pathBounds.top + pathBounds.height() / 2
+        val matrix = Matrix()
+        matrix.setScale(scaleFactor, scaleFactor)
+        matrix.preTranslate(-pathCenterX, -pathCenterY)
+        matrix.postTranslate(width / 2f, height / 2f)
+        path.transform(matrix)
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        setupPath(progress)
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        canvas.drawPath(path, paint)
+    }
+}
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
index ed34b57..c8e30df 100644
--- a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
+++ b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
@@ -26,15 +26,22 @@
 import android.widget.LinearLayout
 import androidx.graphics.shapes.CornerRounding
 import androidx.graphics.shapes.CornerRounding.Companion.Unrounded
+import androidx.graphics.shapes.Morph
 import androidx.graphics.shapes.RoundedPolygon
 import androidx.graphics.shapes.circle
 import androidx.graphics.shapes.rectangle
 import androidx.graphics.shapes.star
+import androidx.graphics.shapes.transformed
 
 class ShapeActivity : Activity() {
 
     val shapes = mutableListOf<RoundedPolygon>()
 
+    lateinit var morphView: MorphView
+
+    lateinit var prevShape: RoundedPolygon
+    lateinit var currShape: RoundedPolygon
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
@@ -47,6 +54,18 @@
         setupShapes()
 
         addShapeViews(container)
+
+        setupMorphView()
+        container.addView(morphView)
+    }
+
+    private fun setupMorphView() {
+        val morph = Morph(prevShape, currShape)
+        if (this::morphView.isInitialized) {
+            morphView.morph = morph
+        } else {
+            morphView = MorphView(this, morph)
+        }
     }
 
     private fun getShapeView(shape: RoundedPolygon, width: Int, height: Int): View {
@@ -59,12 +78,11 @@
     }
 
     private fun setupShapes() {
-        val tmp = FloatArray(2)
         // Note: all RoundedPolygon(4) shapes are placeholders for shapes not yet handled
         val matrix1 = Matrix().apply { setRotate(-45f) }
         val matrix2 = Matrix().apply { setRotate(45f) }
-        val blobR1 = MaterialShapes.blobR(.19f, .86f).transformed(matrix1, tmp)
-        val blobR2 = MaterialShapes.blobR(.19f, .86f).transformed(matrix2, tmp)
+        val blobR1 = MaterialShapes.blobR(.19f, .86f).transformed(matrix1)
+        val blobR2 = MaterialShapes.blobR(.19f, .86f).transformed(matrix2)
 
         //        "Circle" to DefaultShapes.star(4, 1f, 1f),
         shapes.add(RoundedPolygon.circle())
@@ -126,6 +144,9 @@
             MaterialShapes.clover(rounding = .352f, innerRadius = .1f,
             innerRounding = Unrounded))
         shapes.add(RoundedPolygon(3))
+
+        prevShape = shapes[0]
+        currShape = shapes[0]
     }
 
     private fun addShapeViews(container: ViewGroup) {
@@ -143,7 +164,14 @@
                 row.orientation = LinearLayout.HORIZONTAL
                 container.addView(row)
             }
-            row!!.addView(getShapeView(shapes[shapeIndex], WIDTH, HEIGHT))
+            val shape = shapes[shapeIndex]
+            val shapeView = getShapeView(shape, WIDTH, HEIGHT)
+            row!!.addView(shapeView)
+            shapeView.setOnClickListener {
+                prevShape = currShape
+                currShape = shape
+                setupMorphView()
+            }
             ++shapeIndex
         }
     }
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeView.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeView.kt
index 6039e79..61091ca 100644
--- a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeView.kt
+++ b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeView.kt
@@ -20,24 +20,29 @@
 import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Paint
+import android.graphics.Path
 import android.view.View
+import androidx.core.graphics.scaleMatrix
 import androidx.graphics.shapes.RoundedPolygon
+import androidx.graphics.shapes.toPath
 import kotlin.math.min
 
 class ShapeView(context: Context, shape: RoundedPolygon) : View(context) {
     val paint = Paint()
     val shape = shape.normalized()
-    var scale = 1
+    val path = Path()
 
     init {
         paint.setColor(Color.WHITE)
     }
 
     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
-        scale = min(w, h)
+        val scale = min(w, h).toFloat()
+        shape.toPath(path)
+        path.transform(scaleMatrix(scale, scale))
     }
 
     override fun onDraw(canvas: Canvas) {
-        canvas.drawPolygon(shape, scale, paint)
+        canvas.drawPath(path, paint)
     }
 }
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
index 95712e3..b36a5fc 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
@@ -20,8 +20,6 @@
 
 import androidx.annotation.Sampled
 import androidx.health.connect.client.HealthConnectClient
-import androidx.health.connect.client.records.SleepSessionRecord
-import androidx.health.connect.client.records.SleepStageRecord
 import androidx.health.connect.client.records.StepsRecord
 import androidx.health.connect.client.time.TimeRangeFilter
 import java.time.Instant
@@ -50,13 +48,3 @@
         timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
     )
 }
-
-@Sampled
-suspend fun DeleteSleepSession(
-    healthConnectClient: HealthConnectClient,
-    sleepRecord: SleepSessionRecord,
-) {
-    val timeRangeFilter = TimeRangeFilter.between(sleepRecord.startTime, sleepRecord.endTime)
-    healthConnectClient.deleteRecords(SleepSessionRecord::class, timeRangeFilter)
-    healthConnectClient.deleteRecords(SleepStageRecord::class, timeRangeFilter)
-}
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
index 578fd0e..e64ecd0 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
@@ -27,7 +27,6 @@
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.HeartRateRecord
 import androidx.health.connect.client.records.SleepSessionRecord
-import androidx.health.connect.client.records.SleepStageRecord
 import androidx.health.connect.client.records.StepsRecord
 import androidx.health.connect.client.request.ReadRecordsRequest
 import androidx.health.connect.client.time.TimeRangeFilter
@@ -128,17 +127,6 @@
             )
         )
     for (sleepRecord in response.records) {
-        // Process each exercise record
-        // Optionally pull in sleep stages of the same time range
-        val sleepStageRecords =
-            healthConnectClient
-                .readRecords(
-                    ReadRecordsRequest(
-                        SleepStageRecord::class,
-                        timeRangeFilter =
-                            TimeRangeFilter.between(sleepRecord.startTime, sleepRecord.endTime)
-                    )
-                )
-                .records
+        // Process each sleep record
     }
 }
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContractTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContractTest.kt
index 293d577..b734790 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContractTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContractTest.kt
@@ -25,6 +25,7 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -36,6 +37,13 @@
     private val context: Context = ApplicationProvider.getApplicationContext()
 
     @Test
+    fun requestExerciseRoute_createIntent_emptySessionIdThrowsIAE() {
+        assertThrows(IllegalArgumentException::class.java) {
+            ExerciseRouteRequestContract().createIntent(context, "")
+        }
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     fun requestExerciseRoute_createIntent_hasPlatformIntentAction() {
         val intent = ExerciseRouteRequestContract().createIntent(context, "sessionId")
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt
index 0e1e78f..e936736 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt
@@ -52,6 +52,7 @@
      * @see ActivityResultContract.createIntent
      */
     override fun createIntent(context: Context, input: String): Intent {
+        require(input.isNotEmpty()) { "Session identifier can't be empty" }
         return delegate.createIntent(context, input)
     }
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt
index b96640c..a21c0a3 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt
@@ -34,7 +34,6 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 internal class RequestExerciseRouteInternal : ActivityResultContract<String, ExerciseRoute?>() {
     override fun createIntent(context: Context, input: String): Intent {
-        require(input.isNotEmpty()) { "Session identifier can't be empty" }
         return Intent(HealthDataServiceConstants.ACTION_REQUEST_ROUTE).apply {
             putExtra(HealthDataServiceConstants.EXTRA_SESSION_ID, input)
         }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt
index c5ec226..b0c7a42 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt
@@ -38,7 +38,6 @@
 internal class RequestExerciseRouteUpsideDownCake :
     ActivityResultContract<String, ExerciseRoute?>() {
     override fun createIntent(context: Context, input: String): Intent {
-        require(input.isNotEmpty()) { "Session identifier can't be empty" }
         return Intent(HealthConnectManager.ACTION_REQUEST_EXERCISE_ROUTE).apply {
             putExtra(HealthConnectManager.EXTRA_SESSION_ID, input)
         }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
index 679eddb..1bafdd3 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
@@ -35,12 +35,6 @@
         }
     }
 
-    internal fun isWithin(startTime: Instant, endTime: Instant): Boolean {
-        val minTime = route.minBy { it.time }.time
-        val maxTime = route.maxBy { it.time }.time
-        return !minTime.isBefore(startTime) && maxTime.isBefore(endTime)
-    }
-
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is ExerciseRoute) return false
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
index ecd1acf..bfca023 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
@@ -130,8 +130,14 @@
                 "laps can not be out of parent time range."
             }
         }
-        if (exerciseRouteResult is ExerciseRouteResult.Data) {
-            require(exerciseRouteResult.exerciseRoute.isWithin(startTime, endTime)) {
+        if (
+            exerciseRouteResult is ExerciseRouteResult.Data &&
+                exerciseRouteResult.exerciseRoute.route.isNotEmpty()
+        ) {
+            val route = exerciseRouteResult.exerciseRoute.route
+            val minTime = route.minBy { it.time }.time
+            val maxTime = route.maxBy { it.time }.time
+            require(!minTime.isBefore(startTime) && maxTime.isBefore(endTime)) {
                 "route can not be out of parent time range."
             }
         }
@@ -351,9 +357,7 @@
             EXERCISE_TYPE_STRING_TO_INT_MAP.entries.associateBy({ it.value }, { it.key })
     }
 
-    /**
-     * List of supported activities on Health Platform.
-     */
+    /** List of supported activities on Health Platform. */
     @Retention(AnnotationRetention.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @IntDef(
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepSessionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepSessionRecord.kt
index d2a6b91..916fefc 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepSessionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepSessionRecord.kt
@@ -24,32 +24,26 @@
 import java.time.ZoneOffset
 
 /**
- * Captures the user's length and type of sleep. Each record represents a time interval for a stage
- * of sleep.
+ * Captures the user's sleep length and its stages. Each record represents a time interval for a
+ * full sleep session.
  *
- * The start time of the record represents the start of the sleep stage and always needs to be
- * included. The timestamp represents the end of the sleep stage. Time intervals don't need to be
- * continuous but shouldn't overlap.
+ * All sleep stage time intervals should fall within the sleep session interval. Time intervals for
+ * stages don't need to be continuous but shouldn't overlap.
  *
- * Example code demonstrate how to read sleep session with stages:
+ * Example code demonstrate how to read sleep session:
  *
  * @sample androidx.health.connect.client.samples.ReadSleepSessions
- *
- * When deleting a session, associated sleep stage records need to be deleted separately:
- *
- * @sample androidx.health.connect.client.samples.DeleteSleepSession
- * @see SleepStageRecord
  */
-public class SleepSessionRecord(
+class SleepSessionRecord(
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
     /** Title of the session. Optional field. */
-    public val title: String? = null,
+    val title: String? = null,
     /** Additional notes for the session. Optional field. */
-    public val notes: String? = null,
-    public val stages: List<Stage> = emptyList(),
+    val notes: String? = null,
+    val stages: List<Stage> = emptyList(),
     override val metadata: Metadata = Metadata.EMPTY,
 ) : IntervalRecord {
 
@@ -150,9 +144,7 @@
             STAGE_TYPE_STRING_TO_INT_MAP.entries.associateBy({ it.value }, { it.key })
     }
 
-    /**
-     * Type of sleep stage.
-     */
+    /** Type of sleep stage. */
     @Retention(AnnotationRetention.SOURCE)
     @IntDef(
         value =
@@ -175,7 +167,7 @@
      *
      * @see SleepSessionRecord
      */
-    public class Stage(
+    class Stage(
         val startTime: Instant,
         val endTime: Instant,
         @property:StageTypes val stage: Int,
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsTest.kt
deleted file mode 100644
index 99de1ce..0000000
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsTest.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.permission
-
-private const val TEST_PACKAGE = "com.test.app"
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
index 73af184..f4b9255 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
@@ -121,6 +121,99 @@
     }
 
     @Test
+    fun validRecord_emptyRoute_equals() {
+        assertThat(
+                ExerciseSessionRecord(
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = null,
+                    endTime = Instant.ofEpochMilli(1236L),
+                    endZoneOffset = null,
+                    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BIKING,
+                    title = "title",
+                    notes = "notes",
+                    segments =
+                        listOf(
+                            ExerciseSegment(
+                                startTime = Instant.ofEpochMilli(1234L),
+                                endTime = Instant.ofEpochMilli(1235L),
+                                segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                            )
+                        ),
+                    laps =
+                        listOf(
+                            ExerciseLap(
+                                startTime = Instant.ofEpochMilli(1235L),
+                                endTime = Instant.ofEpochMilli(1236L),
+                                length = 10.meters,
+                            )
+                        ),
+                    exerciseRoute = ExerciseRoute(route = listOf()),
+                )
+            )
+            .isEqualTo(
+                ExerciseSessionRecord(
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = null,
+                    endTime = Instant.ofEpochMilli(1236L),
+                    endZoneOffset = null,
+                    exerciseType = EXERCISE_TYPE_BIKING,
+                    title = "title",
+                    notes = "notes",
+                    segments =
+                        listOf(
+                            ExerciseSegment(
+                                startTime = Instant.ofEpochMilli(1234L),
+                                endTime = Instant.ofEpochMilli(1235L),
+                                segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                            )
+                        ),
+                    laps =
+                        listOf(
+                            ExerciseLap(
+                                startTime = Instant.ofEpochMilli(1235L),
+                                endTime = Instant.ofEpochMilli(1236L),
+                                length = 10.meters,
+                            )
+                        ),
+                    exerciseRoute = ExerciseRoute(route = listOf()),
+                )
+            )
+    }
+
+    @Test
+    fun validRecord_emptyRoute_hasExerciseRouteData() {
+        val record =
+            ExerciseSessionRecord(
+                startTime = Instant.ofEpochMilli(1234L),
+                startZoneOffset = null,
+                endTime = Instant.ofEpochMilli(1236L),
+                endZoneOffset = null,
+                exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BIKING,
+                title = "title",
+                notes = "notes",
+                segments =
+                    listOf(
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1235L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                        )
+                    ),
+                laps =
+                    listOf(
+                        ExerciseLap(
+                            startTime = Instant.ofEpochMilli(1235L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                            length = 10.meters,
+                        )
+                    ),
+                exerciseRoute = ExerciseRoute(route = listOf()),
+            )
+        assertThat((record.exerciseRouteResult as ExerciseRouteResult.Data))
+            .isEqualTo(ExerciseRouteResult.Data(ExerciseRoute(listOf())))
+    }
+
+    @Test
     fun invalidTimes_throws() {
         assertFailsWith<IllegalArgumentException> {
             ExerciseSessionRecord(
diff --git a/kruth/kruth/api/current.ignore b/kruth/kruth/api/current.ignore
index a8eda3a..9895958 100644
--- a/kruth/kruth/api/current.ignore
+++ b/kruth/kruth/api/current.ignore
@@ -53,6 +53,14 @@
     Method androidx.kruth.ThrowableSubject.hasCauseThat has changed return type from androidx.kruth.ThrowableSubject to androidx.kruth.ThrowableSubject<java.lang.Throwable>
 
 
+InvalidNullConversion: androidx.kruth.MapSubject#containsEntry(K, V) parameter #0:
+    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter key in androidx.kruth.MapSubject.containsEntry(K key, V value)
+InvalidNullConversion: androidx.kruth.MapSubject#containsEntry(K, V) parameter #1:
+    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter value in androidx.kruth.MapSubject.containsEntry(K key, V value)
+InvalidNullConversion: androidx.kruth.MapSubject#doesNotContainEntry(K, V) parameter #0:
+    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter key in androidx.kruth.MapSubject.doesNotContainEntry(K key, V value)
+InvalidNullConversion: androidx.kruth.MapSubject#doesNotContainEntry(K, V) parameter #1:
+    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter value in androidx.kruth.MapSubject.doesNotContainEntry(K key, V value)
 InvalidNullConversion: androidx.kruth.SimpleSubjectBuilder#that(T) parameter #0:
     Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter actual in androidx.kruth.SimpleSubjectBuilder.that(T actual)
 InvalidNullConversion: androidx.kruth.StandardSubjectBuilder#that(T) parameter #0:
@@ -151,16 +159,10 @@
     Removed method androidx.kruth.MapSubject.comparingValuesUsing(androidx.kruth.Correspondence<? super A,? super E>)
 RemovedMethod: androidx.kruth.MapSubject#containsAtLeast(Object, Object, java.lang.Object...):
     Removed method androidx.kruth.MapSubject.containsAtLeast(Object,Object,java.lang.Object...)
-RemovedMethod: androidx.kruth.MapSubject#containsEntry(Object, Object):
-    Removed method androidx.kruth.MapSubject.containsEntry(Object,Object)
 RemovedMethod: androidx.kruth.MapSubject#containsExactly():
     Removed method androidx.kruth.MapSubject.containsExactly()
 RemovedMethod: androidx.kruth.MapSubject#containsExactly(Object, Object, java.lang.Object...):
     Removed method androidx.kruth.MapSubject.containsExactly(Object,Object,java.lang.Object...)
-RemovedMethod: androidx.kruth.MapSubject#doesNotContainEntry(Object, Object):
-    Removed method androidx.kruth.MapSubject.doesNotContainEntry(Object,Object)
-RemovedMethod: androidx.kruth.MapSubject#doesNotContainKey(Object):
-    Removed method androidx.kruth.MapSubject.doesNotContainKey(Object)
 RemovedMethod: androidx.kruth.MapSubject#formattingDiffsUsing(androidx.kruth.Correspondence.DiffFormatter<? super V,? super V>):
     Removed method androidx.kruth.MapSubject.formattingDiffsUsing(androidx.kruth.Correspondence.DiffFormatter<? super V,? super V>)
 RemovedMethod: androidx.kruth.StandardSubjectBuilder#about(androidx.kruth.CustomSubjectBuilder.Factory<CustomSubjectBuilderT>):
diff --git a/kruth/kruth/api/current.txt b/kruth/kruth/api/current.txt
index 74399e0..95b8112 100644
--- a/kruth/kruth/api/current.txt
+++ b/kruth/kruth/api/current.txt
@@ -128,9 +128,14 @@
   public final class MapSubject<K, V> extends androidx.kruth.Subject<java.util.Map<K,? extends V>> {
     method public androidx.kruth.Ordered containsAtLeast(kotlin.Pair<? extends K,? extends V>... entries);
     method public androidx.kruth.Ordered containsAtLeastEntriesIn(java.util.Map<K,? extends V> expectedMap);
+    method public void containsEntry(K key, V value);
+    method public void containsEntry(kotlin.Pair<? extends K,? extends V> entry);
     method public androidx.kruth.Ordered containsExactly(kotlin.Pair<? extends K,? extends V>... entries);
     method public androidx.kruth.Ordered containsExactlyEntriesIn(java.util.Map<K,? extends V> expectedMap);
     method public void containsKey(Object? key);
+    method public void doesNotContainEntry(K key, V value);
+    method public void doesNotContainEntry(kotlin.Pair<? extends K,? extends V> entry);
+    method public void doesNotContainKey(Object? key);
     method public void hasSize(int expectedSize);
     method public void isEmpty();
     method public void isNotEmpty();
diff --git a/kruth/kruth/api/restricted_current.ignore b/kruth/kruth/api/restricted_current.ignore
index a8eda3a..9895958 100644
--- a/kruth/kruth/api/restricted_current.ignore
+++ b/kruth/kruth/api/restricted_current.ignore
@@ -53,6 +53,14 @@
     Method androidx.kruth.ThrowableSubject.hasCauseThat has changed return type from androidx.kruth.ThrowableSubject to androidx.kruth.ThrowableSubject<java.lang.Throwable>
 
 
+InvalidNullConversion: androidx.kruth.MapSubject#containsEntry(K, V) parameter #0:
+    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter key in androidx.kruth.MapSubject.containsEntry(K key, V value)
+InvalidNullConversion: androidx.kruth.MapSubject#containsEntry(K, V) parameter #1:
+    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter value in androidx.kruth.MapSubject.containsEntry(K key, V value)
+InvalidNullConversion: androidx.kruth.MapSubject#doesNotContainEntry(K, V) parameter #0:
+    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter key in androidx.kruth.MapSubject.doesNotContainEntry(K key, V value)
+InvalidNullConversion: androidx.kruth.MapSubject#doesNotContainEntry(K, V) parameter #1:
+    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter value in androidx.kruth.MapSubject.doesNotContainEntry(K key, V value)
 InvalidNullConversion: androidx.kruth.SimpleSubjectBuilder#that(T) parameter #0:
     Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter actual in androidx.kruth.SimpleSubjectBuilder.that(T actual)
 InvalidNullConversion: androidx.kruth.StandardSubjectBuilder#that(T) parameter #0:
@@ -151,16 +159,10 @@
     Removed method androidx.kruth.MapSubject.comparingValuesUsing(androidx.kruth.Correspondence<? super A,? super E>)
 RemovedMethod: androidx.kruth.MapSubject#containsAtLeast(Object, Object, java.lang.Object...):
     Removed method androidx.kruth.MapSubject.containsAtLeast(Object,Object,java.lang.Object...)
-RemovedMethod: androidx.kruth.MapSubject#containsEntry(Object, Object):
-    Removed method androidx.kruth.MapSubject.containsEntry(Object,Object)
 RemovedMethod: androidx.kruth.MapSubject#containsExactly():
     Removed method androidx.kruth.MapSubject.containsExactly()
 RemovedMethod: androidx.kruth.MapSubject#containsExactly(Object, Object, java.lang.Object...):
     Removed method androidx.kruth.MapSubject.containsExactly(Object,Object,java.lang.Object...)
-RemovedMethod: androidx.kruth.MapSubject#doesNotContainEntry(Object, Object):
-    Removed method androidx.kruth.MapSubject.doesNotContainEntry(Object,Object)
-RemovedMethod: androidx.kruth.MapSubject#doesNotContainKey(Object):
-    Removed method androidx.kruth.MapSubject.doesNotContainKey(Object)
 RemovedMethod: androidx.kruth.MapSubject#formattingDiffsUsing(androidx.kruth.Correspondence.DiffFormatter<? super V,? super V>):
     Removed method androidx.kruth.MapSubject.formattingDiffsUsing(androidx.kruth.Correspondence.DiffFormatter<? super V,? super V>)
 RemovedMethod: androidx.kruth.StandardSubjectBuilder#about(androidx.kruth.CustomSubjectBuilder.Factory<CustomSubjectBuilderT>):
diff --git a/kruth/kruth/api/restricted_current.txt b/kruth/kruth/api/restricted_current.txt
index ca3e549..ebdd392 100644
--- a/kruth/kruth/api/restricted_current.txt
+++ b/kruth/kruth/api/restricted_current.txt
@@ -128,9 +128,14 @@
   public final class MapSubject<K, V> extends androidx.kruth.Subject<java.util.Map<K,? extends V>> {
     method public androidx.kruth.Ordered containsAtLeast(kotlin.Pair<? extends K,? extends V>... entries);
     method public androidx.kruth.Ordered containsAtLeastEntriesIn(java.util.Map<K,? extends V> expectedMap);
+    method public void containsEntry(K key, V value);
+    method public void containsEntry(kotlin.Pair<? extends K,? extends V> entry);
     method public androidx.kruth.Ordered containsExactly(kotlin.Pair<? extends K,? extends V>... entries);
     method public androidx.kruth.Ordered containsExactlyEntriesIn(java.util.Map<K,? extends V> expectedMap);
     method public void containsKey(Object? key);
+    method public void doesNotContainEntry(K key, V value);
+    method public void doesNotContainEntry(kotlin.Pair<? extends K,? extends V> entry);
+    method public void doesNotContainKey(Object? key);
     method public void hasSize(int expectedSize);
     method public void isEmpty();
     method public void isNotEmpty();
diff --git a/kruth/kruth/src/commonMain/kotlin/androidx/kruth/MapSubject.kt b/kruth/kruth/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
index dda178d..2c30506 100644
--- a/kruth/kruth/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
+++ b/kruth/kruth/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
@@ -16,6 +16,9 @@
 
 package androidx.kruth
 
+import androidx.kruth.Fact.Companion.fact
+import androidx.kruth.Fact.Companion.simpleFact
+
 class MapSubject<K, V> internal constructor(
     actual: Map<K, V>?,
     metadata: FailureMetadata = FailureMetadata(),
@@ -55,6 +58,97 @@
         }
     }
 
+    /** Fails if the map contains the given key.  */
+    fun doesNotContainKey(key: Any?) {
+        requireNonNull(actual) { "Expected not to contain $key, but was null" }
+        if (key in actual) {
+            failWithoutActual(fact("Expected not to contain", key), fact("but was", actual.keys))
+        }
+    }
+
+    /** Fails if the map does not contain the given entry.  */
+    fun containsEntry(key: K, value: V) {
+        val entry = key to value
+
+        requireNonNull(actual) { "Expected to contain $entry, but was null" }
+
+        if (actual.entries.any { (k, v) -> (k == key) && (v == value) }) {
+            return
+        }
+
+        val keyList = listOf(key)
+        val valueList = listOf(value)
+
+        if (key in actual) {
+            val actualValue = actual[key]
+            /*
+             * In the case of a null expected or actual value, clarify that the key *is* present
+             * and *is* expected to be present. That is, get() isn't returning null to indicate
+             * that the key is missing, and the user isn't making an assertion that the key is
+             * missing.
+             */
+            if ((value == null) || (actualValue == null)) {
+                failWithActual(
+                    fact("Expected to contain entry", entry),
+                    fact("key is present but with a different value"),
+                )
+            } else {
+                failWithActual(fact("Expected to contain entry", entry))
+            }
+        } else if (actual.keys.hasMatchingToStringPair(keyList)) {
+            failWithActual(
+                fact("Expected to contain entry", entry),
+                fact("an instance of", entry.typeName()),
+                simpleFact("but did not"),
+                fact(
+                    "though it did contain keys",
+                    actual.keys.retainMatchingToString(keyList).countDuplicatesAndAddTypeInfo(),
+                ),
+            )
+        } else if (actual.containsValue(value)) {
+            val keys = actual.filterValues { it == value }.keys
+            failWithActual(
+                fact("Expected to contain entry", entry),
+                simpleFact("but did not"),
+                fact("though it did contain keys with that value", keys),
+            )
+        } else if (actual.values.hasMatchingToStringPair(valueList)) {
+            failWithActual(
+                fact("Expected to contain entry", entry),
+                fact("an instance of", entry.typeName()),
+                simpleFact("but did not"),
+                fact(
+                    "though it did contain values",
+                    actual.values.retainMatchingToString(valueList)
+                        .countDuplicatesAndAddTypeInfo(),
+                ),
+            )
+        } else {
+            failWithActual(fact("Expected to contain entry", entry))
+        }
+    }
+
+    /** Fails if the map does not contain the given entry.  */
+    fun containsEntry(entry: Pair<K, V>) {
+        containsEntry(key = entry.first, value = entry.second)
+    }
+
+    /** Fails if the map contains the given entry. */
+    fun doesNotContainEntry(key: K, value: V) {
+        val entry = key to value
+
+        requireNonNull(actual) { "Expected not to contain $entry, but was null" }
+
+        if (actual.entries.any { (k, v) -> (k == key) && (v == value) }) {
+            failWithActual(fact("Expected not to contain", entry))
+        }
+    }
+
+    /** Fails if the map contains the given entry. */
+    fun doesNotContainEntry(entry: Pair<K, V>) {
+        doesNotContainEntry(key = entry.first, value = entry.second)
+    }
+
     /**
      * Fails if the map does not contain exactly the given set of key/value pairs. The arguments
      * must not contain duplicate keys.
diff --git a/kruth/kruth/src/commonMain/kotlin/androidx/kruth/Utils.kt b/kruth/kruth/src/commonMain/kotlin/androidx/kruth/Utils.kt
index 1fc2292..5ffb9fa 100644
--- a/kruth/kruth/src/commonMain/kotlin/androidx/kruth/Utils.kt
+++ b/kruth/kruth/src/commonMain/kotlin/androidx/kruth/Utils.kt
@@ -57,3 +57,68 @@
         (list != null) && (item !in list)
     }
 }
+
+internal fun Iterable<*>.hasMatchingToStringPair(items: Iterable<*>): Boolean =
+    if (isEmpty() || items.isEmpty()) {
+        false // Bail early to avoid calling hashCode() on the elements unnecessarily.
+    } else {
+        retainMatchingToString(items).isNotEmpty()
+    }
+
+internal fun Any?.typeName(): String =
+    when (this) {
+        null -> {
+            // The name "null type" comes from the interface javax.lang.model.type.NullType
+            "null type"
+        }
+
+        is Map.Entry<*, *> -> {
+            // Fix for interesting bug when entry.getValue() returns itself b/170390717
+            val valueTypeName = if (value === this) "Map.Entry" else value.typeName()
+            "Map.Entry<${key.typeName()}, $valueTypeName>"
+        }
+
+        else -> this::class.simpleName ?: "unknown type"
+    }
+
+internal fun Iterable<*>.countDuplicatesAndAddTypeInfo(): String {
+    val homogeneousTypeName = homogeneousTypeName()
+
+    return if (homogeneousTypeName != null) {
+        "${countDuplicates()} ($homogeneousTypeName)"
+    } else {
+        addTypeInfoToEveryItem().countDuplicates()
+    }
+}
+
+internal fun Iterable<*>.countDuplicates(): String =
+    groupingBy { it }
+        .eachCount()
+        .entries
+        .joinToString(
+            prefix = "[",
+            postfix = "]",
+            transform = { (item, count) ->
+                if (count > 1) "$item [$count copies]" else item.toString()
+            },
+        )
+
+/**
+ * Returns the name of the single type of all given items or `null` if no such type exists.
+ */
+private fun Iterable<*>.homogeneousTypeName(): String? {
+    var homogeneousTypeName: String? = null
+
+    for (item in this) {
+        when {
+            item == null -> return null
+            homogeneousTypeName == null -> homogeneousTypeName = item.typeName() // First item
+            item.typeName() != homogeneousTypeName -> return null // Heterogeneous collection
+        }
+    }
+
+    return homogeneousTypeName
+}
+
+private fun Iterable<*>.addTypeInfoToEveryItem(): List<String> =
+    map { "$it (${it.typeName()})" }
diff --git a/kruth/kruth/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt b/kruth/kruth/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt
index f380fc7..d2d3d73 100644
--- a/kruth/kruth/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt
+++ b/kruth/kruth/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt
@@ -449,6 +449,129 @@
     }
 
     @Test
+    fun doesNotContainKey() {
+        val actual = mapOf("kurt" to "kluever")
+        assertThat(actual).doesNotContainKey("greg")
+        assertThat(actual).doesNotContainKey(null)
+    }
+
+    @Test
+    fun doesNotContainKeyFailure() {
+        val actual = mapOf("kurt" to "kluever")
+        assertFailsWith<AssertionError> {
+            assertThat(actual).doesNotContainKey("kurt")
+        }
+    }
+
+    @Test
+    fun doesNotContainNullKey() {
+        val actual = mapOf<String?, String>(null to "null")
+        assertFailsWith<AssertionError> {
+            assertThat(actual).doesNotContainKey(null)
+        }
+    }
+
+    @Test
+    fun containsEntry() {
+        val actual = mapOf("kurt" to "kluever")
+        assertThat(actual).containsEntry("kurt" to "kluever")
+    }
+
+    @Test
+    fun containsEntryFailure() {
+        val actual = mapOf("kurt" to "kluever")
+        assertFailsWith<AssertionError> {
+            assertThat(actual).containsEntry("greg" to "kick")
+        }
+    }
+
+    @Test
+    fun containsEntry_failsWithSameToStringOfKey() {
+        val actual = mapOf<Number, String>(1L to "value1", 2L to "value2")
+        assertFailsWith<AssertionError> {
+            assertThat(actual).containsEntry(1 to "value1")
+        }
+    }
+
+    @Test
+    fun containsEntry_failsWithSameToStringOfValue() {
+        // Does not contain the correct key, but does contain a value which matches by toString.
+        assertFailsWith<AssertionError> {
+            assertThat(mapOf<Int, String?>(1 to "null")).containsEntry(2 to null)
+        }
+    }
+
+    @Test
+    fun containsNullKeyAndValue() {
+        val actual = mapOf<String?, String?>("kurt" to "kluever")
+        assertFailsWith<AssertionError> {
+            assertThat(actual).containsEntry(null to null)
+        }
+    }
+
+    @Test
+    fun containsNullEntry() {
+        val actual = mapOf<String?, String?>(null to null)
+        assertThat(actual).containsEntry(null to null)
+    }
+
+    @Test
+    fun containsNullEntryValue() {
+        val actual = mapOf<String?, String?>(null to null)
+        assertFailsWith<AssertionError> {
+            assertThat(actual).containsEntry("kurt" to null)
+        }
+    }
+
+    @Test
+    fun containsNullEntryKey() {
+        val actual = mapOf<String?, String?>(null to null)
+        assertFailsWith<AssertionError> {
+            assertThat(actual).containsEntry(null to "kluever")
+        }
+    }
+
+    @Test
+    fun containsExactly_bothExactAndToStringKeyMatches_showsExactKeyMatch() {
+        val actual = mapOf<Number, String>(1 to "actual int", 1L to "actual long")
+        assertFailsWith<AssertionError> {
+            assertThat(actual).containsEntry(1L to "expected long")
+        }
+    }
+
+    @Test
+    fun doesNotContainEntry() {
+        val actual = mapOf<String?, String?>("kurt" to "kluever")
+        assertThat(actual).doesNotContainEntry("greg" to "kick")
+        assertThat(actual).doesNotContainEntry(null to null)
+        assertThat(actual).doesNotContainEntry("kurt" to null)
+        assertThat(actual).doesNotContainEntry(null to "kluever")
+    }
+
+    @Test
+    fun doesNotContainEntryFailure() {
+        val actual = mapOf<String?, String?>("kurt" to "kluever")
+        assertFailsWith<AssertionError> {
+            assertThat(actual).doesNotContainEntry("kurt" to "kluever")
+        }
+    }
+
+    @Test
+    fun doesNotContainNullEntry() {
+        val actual = mapOf<String?, String?>(null to null)
+        assertThat(actual).doesNotContainEntry("kurt" to null)
+        assertThat(actual).doesNotContainEntry(null to "kluever")
+    }
+
+    @Test
+    fun doesNotContainNullEntryFailure() {
+        val actual = mapOf<String?, String?>(null to null)
+        assertFailsWith<AssertionError> {
+            assertThat(actual).doesNotContainEntry(null to null)
+        }
+    }
+
+    @Test
     fun failMapContainsKey() {
         assertFailsWith<AssertionError> {
             assertThat(mapOf("a" to "A")).containsKey("b")
diff --git a/libraryversions.toml b/libraryversions.toml
index a1ce48a..0e3bb10 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,5 +1,5 @@
 [versions]
-ACTIVITY = "1.8.0-alpha08"
+ACTIVITY = "1.8.0-beta01"
 ANNOTATION = "1.8.0-alpha01"
 ANNOTATION_EXPERIMENTAL = "1.4.0-alpha01"
 APPACTIONS_BUILTINTYPES = "1.0.0-alpha01"
@@ -37,10 +37,10 @@
 CORE_HAPTICS = "1.0.0-alpha01"
 CORE_I18N = "1.0.0-alpha02"
 CORE_LOCATION_ALTITUDE = "1.0.0-alpha02"
-CORE_PERFORMANCE = "1.0.0-alpha04"
+CORE_PERFORMANCE = "1.0.0-beta01"
 CORE_REMOTEVIEWS = "1.1.0-alpha01"
 CORE_ROLE = "1.2.0-alpha01"
-CORE_SPLASHSCREEN = "1.1.0-alpha01"
+CORE_SPLASHSCREEN = "1.1.0-alpha02"
 CORE_TELECOM = "1.0.0-alpha01"
 CORE_UWB = "1.0.0-alpha08"
 CREDENTIALS = "1.2.0-beta04"
@@ -110,7 +110,7 @@
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
 REMOTECALLBACK = "1.0.0-alpha02"
 RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.6.0-beta02"
+ROOM = "2.6.0-rc01"
 SAFEPARCEL = "1.0.0-alpha01"
 SAVEDSTATE = "1.3.0-alpha01"
 SECURITY = "1.1.0-alpha07"
@@ -124,7 +124,7 @@
 SLICE_BUILDERS_KTX = "1.0.0-alpha08"
 SLICE_REMOTECALLBACK = "1.0.0-alpha01"
 SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.4.0-beta02"
+SQLITE = "2.4.0-rc01"
 SQLITE_INSPECTOR = "2.1.0-alpha01"
 STABLE_AIDL = "1.0.0-alpha01"
 STARTUP = "1.2.0-alpha03"
@@ -135,7 +135,7 @@
 TEXT = "1.0.0-alpha01"
 TRACING = "1.3.0-alpha02"
 TRACING_PERFETTO = "1.0.0-beta03"
-TRANSITION = "1.5.0-alpha01"
+TRANSITION = "1.5.0-alpha02"
 TV = "1.0.0-alpha09"
 TVPROVIDER = "1.1.0-alpha02"
 VECTORDRAWABLE = "1.2.0-rc01"
@@ -146,7 +146,7 @@
 VIEWPAGER2 = "1.1.0-beta03"
 WEAR = "1.4.0-alpha01"
 WEAR_COMPOSE = "1.3.0-alpha05"
-WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha10"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha11"
 WEAR_INPUT = "1.2.0-alpha03"
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha02"
diff --git a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
index c8f3cc7..2d0872e 100644
--- a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
@@ -45,10 +45,14 @@
 @JvmName("map")
 @MainThread
 @CheckResult
+@Suppress("UNCHECKED_CAST")
 fun <X, Y> LiveData<X>.map(
     transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards Y)
 ): LiveData<Y> {
     val result = MediatorLiveData<Y>()
+    if (isInitialized) {
+        result.value = transform(value as X)
+    }
     result.addSource(this) { x -> result.value = transform(x) }
     return result
 }
@@ -113,18 +117,21 @@
 @JvmName("switchMap")
 @MainThread
 @CheckResult
+@Suppress("UNCHECKED_CAST")
 fun <X, Y> LiveData<X>.switchMap(
     transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards LiveData<Y>)?
 ): LiveData<Y> {
     val result = MediatorLiveData<Y>()
-    result.addSource(this, object : Observer<X> {
-        var liveData: LiveData<Y>? = null
-
-        override fun onChanged(value: X) {
-            val newLiveData = transform(value)
-            if (liveData === newLiveData) {
-                return
-            }
+    var liveData: LiveData<Y>? = null
+    if (isInitialized) {
+        liveData = transform(value as X)
+        if (liveData != null && liveData.isInitialized) {
+            result.value = liveData.value
+        }
+    }
+    result.addSource(this) { value: X ->
+        val newLiveData = transform(value)
+        if (liveData !== newLiveData) {
             if (liveData != null) {
                 result.removeSource(liveData!!)
             }
@@ -133,7 +140,7 @@
                 result.addSource(liveData!!) { y -> result.setValue(y) }
             }
         }
-    })
+    }
     return result
 }
 
diff --git a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.kt b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.kt
index 737abdb..e343219 100644
--- a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.kt
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.kt
@@ -64,6 +64,26 @@
     }
 
     @Test
+    fun testMap_initialValueIsSet() {
+        val initialValue = "value"
+        val source = MutableLiveData(initialValue)
+        val mapped = source.map { it }
+        assertThat(mapped.isInitialized, `is`(true))
+        assertThat(source.value, `is`(initialValue))
+        assertThat(mapped.value, `is`(initialValue))
+    }
+
+    @Test
+    fun testMap_initialValueNull() {
+        val source = MutableLiveData<String?>(null)
+        val output = "testOutput"
+        val mapped: LiveData<String?> = source.map { output }
+        assertThat(mapped.isInitialized, `is`(true))
+        assertThat(source.value, nullValue())
+        assertThat(mapped.value, `is`(output))
+    }
+
+    @Test
     fun testSwitchMap() {
         val trigger: LiveData<Int> = MutableLiveData()
         val first: LiveData<String> = MutableLiveData()
@@ -123,6 +143,36 @@
     }
 
     @Test
+    fun testSwitchMap_initialValueSet() {
+        val initialValue1 = "value1"
+        val original = MutableLiveData(true)
+        val source1 = MutableLiveData(initialValue1)
+
+        val switched = original.switchMap { source1 }
+        assertThat(switched.isInitialized, `is`(true))
+        assertThat(source1.value, `is`(initialValue1))
+        assertThat(switched.value, `is`(initialValue1))
+    }
+
+    @Test
+    fun testSwitchMap_noInitialValue_notInitialized() {
+        val original = MutableLiveData(true)
+        val source = MutableLiveData<String>()
+
+        val switched = original.switchMap { source }
+        assertThat(switched.isInitialized, `is`(false))
+    }
+
+    @Test
+    fun testSwitchMap_initialValueNull() {
+        val original = MutableLiveData<String?>(null)
+        val source = MutableLiveData<String?>()
+
+        val switched = original.switchMap { source }
+        assertThat(switched.isInitialized, `is`(false))
+    }
+
+    @Test
     fun testNoRedispatchSwitchMap() {
         val trigger: LiveData<Int> = MutableLiveData()
         val first: LiveData<String> = MutableLiveData()
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleSupportTest.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleSupportTest.kt
index 12eff00..4151c58 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleSupportTest.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleSupportTest.kt
@@ -17,6 +17,8 @@
 package androidx.lifecycle.viewmodel.savedstate
 
 import android.os.Bundle
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
 import androidx.lifecycle.enableSavedStateHandles
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -69,6 +71,31 @@
 
     @UiThreadTest
     @Test
+    fun testSavedStateHandleSupportWithActivityDestroyed() {
+        val component = TestComponent()
+        component.enableSavedStateHandles()
+        val handle = component.createSavedStateHandle("test")
+        component.resume()
+        handle.set("a", "1")
+        val interim = component.recreate(keepingViewModels = true)
+        interim.enableSavedStateHandles()
+        handle.set("b", "2")
+        interim.resume()
+
+        val recreated = interim.recreate(keepingViewModels = false)
+        recreated.enableSavedStateHandles()
+        (recreated.lifecycle as LifecycleRegistry).currentState = Lifecycle.State.CREATED
+        // during activity recreation, perform save may be called during restore, ensure
+        // this performSave does not override the state that has been restored
+        recreated.performSave(Bundle())
+        val restoredHandle = recreated.createSavedStateHandle("test")
+
+        assertThat(restoredHandle.get<String>("a")).isEqualTo("1")
+        assertThat(restoredHandle.get<String>("b")).isEqualTo("2")
+    }
+
+    @UiThreadTest
+    @Test
     fun failWithNoInstallation() {
         val component = TestComponent()
         try {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
index f28e101..d28c8f0 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
@@ -59,12 +59,16 @@
     fun recreate(keepingViewModels: Boolean): TestComponent {
         val bundle = Bundle()
         lifecycleRegistry.currentState = Lifecycle.State.CREATED
-        savedStateController.performSave(bundle)
+        performSave(bundle)
         lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
         if (!keepingViewModels) vmStore.clear()
         return TestComponent(vmStore.takeIf { keepingViewModels } ?: ViewModelStore(), bundle)
     }
 
+    fun performSave(bundle: Bundle) {
+        savedStateController.performSave(bundle)
+    }
+
     fun createSavedStateHandle(key: String, bundle: Bundle? = null): SavedStateHandle {
         val extras = MutableCreationExtras()
         extras[VIEW_MODEL_STORE_OWNER_KEY] = this
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
index 55bbe1e..c362b67 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
@@ -161,7 +161,11 @@
      */
     fun performRestore() {
         if (!restored) {
-            restoredState = savedStateRegistry.consumeRestoredStateForKey(SAVED_STATE_KEY)
+            val newState = savedStateRegistry.consumeRestoredStateForKey(SAVED_STATE_KEY)
+            restoredState = Bundle().apply {
+                restoredState?.let { putAll(it) }
+                newState?.let { putAll(it) }
+            }
             restored = true
             // Grab a reference to the ViewModel for later usage when we saveState()
             // This ensures that even if saveState() is called after the Lifecycle is
diff --git a/lint-checks/src/main/java/androidx/build/lint/UnstableAidlAnnotationDetector.kt b/lint-checks/src/main/java/androidx/build/lint/UnstableAidlAnnotationDetector.kt
index 637bee0..8a9be96 100644
--- a/lint-checks/src/main/java/androidx/build/lint/UnstableAidlAnnotationDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/UnstableAidlAnnotationDetector.kt
@@ -40,6 +40,9 @@
  */
 private const val JAVA_PASSTHROUGH = "JavaPassthrough"
 
+private const val ANDROIDX_REQUIRESOPTIN = "androidx.annotation.RequiresOptIn"
+private const val KOTLIN_REQUIRESOPTIN = "kotlin.RequiresOptIn"
+
 class UnstableAidlAnnotationDetector : AidlDefinitionDetector() {
 
     override fun visitAidlParcelableDeclaration(context: Context, node: AidlParcelableDeclaration) {
@@ -84,8 +87,8 @@
                 )
                 // Determine if the class is annotated with RequiresOptIn.
                 psiClass?.annotations?.any { psiAnnotation ->
-                    // Either androidx.annotation or kotlin version is fine here.
-                    psiAnnotation.hasQualifiedName("RequiresOptIn")
+                    psiAnnotation.hasQualifiedName(ANDROIDX_REQUIRESOPTIN) ||
+                        psiAnnotation.hasQualifiedName(KOTLIN_REQUIRESOPTIN)
                 } ?: false
             } else {
                 false
diff --git a/lint-checks/src/test/java/androidx/build/lint/UnstableAidlAnnotationDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/UnstableAidlAnnotationDetectorTest.kt
index 8789d3b..c8c9885 100644
--- a/lint-checks/src/test/java/androidx/build/lint/UnstableAidlAnnotationDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/UnstableAidlAnnotationDetectorTest.kt
@@ -131,6 +131,8 @@
             java(
                 "src/androidx/core/UnstableAidlDefinition.java",
                 """
+                    import androidx.annotation.RequiresOptIn;
+
                     @RequiresOptIn
                     public @interface UnstableAidlDefinition {}
                 """.trimIndent()
@@ -168,7 +170,7 @@
             java(
                 "src/androidx/core/UnstableAidlDefinition.java",
                 """
-                    @RequiresOptIn
+                    @androidx.annotation.RequiresOptIn
                     public @interface UnstableAidlDefinition {}
                 """.trimIndent()
             ),
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index 02cb445..231c532 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -74,45 +74,29 @@
 
     static final String TAG = "GlobalMediaRouter";
     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-    final Context mApplicationContext;
-    SystemMediaRouteProvider mSystemProvider;
+
+    final CallbackHandler mCallbackHandler = new CallbackHandler();
+    // A map from unique route ID to RouteController for the member routes in the currently
+    // selected route group.
+    final Map<String, MediaRouteProvider.RouteController> mRouteControllerMap = new HashMap<>();
+
     @VisibleForTesting
     RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
-    boolean mTransferReceiverDeclared;
-    MediaRoute2Provider mMr2Provider;
+    MediaRouter.RouteInfo mSelectedRoute;
+    MediaRouteProvider.RouteController mSelectedRouteController;
+    MediaRouter.OnPrepareTransferListener mOnPrepareTransferListener;
+    MediaRouter.PrepareTransferNotifier mTransferNotifier;
 
-    final ArrayList<WeakReference<MediaRouter>> mRouters = new ArrayList<>();
+    private final Context mApplicationContext;
+    private final ArrayList<WeakReference<MediaRouter>> mRouters = new ArrayList<>();
     private final ArrayList<MediaRouter.RouteInfo> mRoutes = new ArrayList<>();
     private final Map<Pair<String, String>, String> mUniqueIdMap = new HashMap<>();
     private final ArrayList<MediaRouter.ProviderInfo> mProviders = new ArrayList<>();
     private final ArrayList<RemoteControlClientRecord> mRemoteControlClients = new ArrayList<>();
-    final RemoteControlClientCompat.PlaybackInfo mPlaybackInfo =
+    private final RemoteControlClientCompat.PlaybackInfo mPlaybackInfo =
             new RemoteControlClientCompat.PlaybackInfo();
     private final ProviderCallback mProviderCallback = new ProviderCallback();
-    final CallbackHandler mCallbackHandler = new CallbackHandler();
-    private DisplayManagerCompat mDisplayManager;
     private final boolean mLowRam;
-    private MediaRouterActiveScanThrottlingHelper mActiveScanThrottlingHelper;
-
-    private MediaRouterParams mRouterParams;
-    MediaRouter.RouteInfo mDefaultRoute;
-    private MediaRouter.RouteInfo mBluetoothRoute;
-    MediaRouter.RouteInfo mSelectedRoute;
-    MediaRouteProvider.RouteController mSelectedRouteController;
-    // Represents a route that are requested to be selected asynchronously.
-    MediaRouter.RouteInfo mRequestedRoute;
-    MediaRouteProvider.RouteController mRequestedRouteController;
-    // A map from unique route ID to RouteController for the member routes in the currently
-    // selected route group.
-    final Map<String, MediaRouteProvider.RouteController> mRouteControllerMap = new HashMap<>();
-    private MediaRouteDiscoveryRequest mDiscoveryRequest;
-    private MediaRouteDiscoveryRequest mDiscoveryRequestForMr2Provider;
-    private int mCallbackCount;
-    MediaRouter.OnPrepareTransferListener mOnPrepareTransferListener;
-    MediaRouter.PrepareTransferNotifier mTransferNotifier;
-    private MediaSessionRecord mMediaSession;
-    MediaSessionCompat mRccMediaSession;
-    private MediaSessionCompat mCompatSession;
     private final MediaSessionCompat.OnActiveChangeListener mSessionActiveListener =
             new MediaSessionCompat.OnActiveChangeListener() {
                 @Override
@@ -130,6 +114,25 @@
                 }
             };
 
+    private boolean mTransferReceiverDeclared;
+    private boolean mUseMediaRouter2ForSystemRouting;
+    private MediaRoute2Provider mMr2Provider;
+    private SystemMediaRouteProvider mSystemProvider;
+    private DisplayManagerCompat mDisplayManager;
+    private MediaRouterActiveScanThrottlingHelper mActiveScanThrottlingHelper;
+    private MediaRouterParams mRouterParams;
+    private MediaRouter.RouteInfo mDefaultRoute;
+    private MediaRouter.RouteInfo mBluetoothRoute;
+    // Represents a route that are requested to be selected asynchronously.
+    private MediaRouter.RouteInfo mRequestedRoute;
+    private MediaRouteProvider.RouteController mRequestedRouteController;
+    private MediaRouteDiscoveryRequest mDiscoveryRequest;
+    private MediaRouteDiscoveryRequest mDiscoveryRequestForMr2Provider;
+    private int mCallbackCount;
+    private MediaSessionRecord mMediaSession;
+    private MediaSessionCompat mRccMediaSession;
+    private MediaSessionCompat mCompatSession;
+
     /* package */ GlobalMediaRouter(Context applicationContext) {
         mApplicationContext = applicationContext;
         mLowRam =
@@ -140,6 +143,15 @@
         mTransferReceiverDeclared =
                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
                         && MediaTransferReceiver.isDeclared(mApplicationContext);
+        mUseMediaRouter2ForSystemRouting =
+                SystemRoutingUsingMediaRouter2Receiver.isDeclared(mApplicationContext);
+
+        if (DEBUG && mUseMediaRouter2ForSystemRouting) {
+            // This is only added to skip the presubmit check for UnusedVariable
+            // TODO: Remove it once mUseMediaRouter2ForSystemRouting is actually used
+            Log.d(TAG, "Using MediaRouter2 for system routing");
+        }
+
         mMr2Provider =
                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && mTransferReceiverDeclared
                         ? new MediaRoute2Provider(mApplicationContext, new Mr2ProviderCallback())
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/SystemRoutingUsingMediaRouter2Receiver.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/SystemRoutingUsingMediaRouter2Receiver.java
new file mode 100644
index 0000000..ecc6df6
--- /dev/null
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/SystemRoutingUsingMediaRouter2Receiver.java
@@ -0,0 +1,57 @@
+/*
+ * 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.mediarouter.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.util.List;
+
+/**
+ * A {@link BroadcastReceiver} class for enabling apps to get SystemRoutes using
+ * {@link android.media.MediaRouter2}.
+ */
+@RestrictTo(LIBRARY)
+final class SystemRoutingUsingMediaRouter2Receiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+        // Do nothing for now.
+    }
+
+    /**
+     * Checks whether the {@link SystemRoutingUsingMediaRouter2Receiver} is declared in the app's
+     * manifest.
+     */
+    @RestrictTo(LIBRARY)
+    public static boolean isDeclared(@NonNull Context applicationContext) {
+        Intent queryIntent = new Intent(applicationContext,
+                SystemRoutingUsingMediaRouter2Receiver.class);
+        queryIntent.setPackage(applicationContext.getPackageName());
+        PackageManager pm = applicationContext.getPackageManager();
+        List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
+
+        return resolveInfos.size() > 0;
+    }
+}
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index c549a7d..7102eda 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -34,11 +34,11 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.lifecycle:lifecycle-common:2.6.1")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
+    api("androidx.lifecycle:lifecycle-common:2.6.2")
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
     api("androidx.savedstate:savedstate-ktx:1.2.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
     implementation("androidx.core:core-ktx:1.1.0")
     implementation("androidx.collection:collection-ktx:1.1.0")
     implementation("androidx.profileinstaller:profileinstaller:1.3.0")
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 30482d2..519c0c9 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -33,7 +33,7 @@
     api("androidx.compose.runtime:runtime:1.5.1")
     api("androidx.compose.runtime:runtime-saveable:1.5.1")
     api("androidx.compose.ui:ui:1.5.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
     api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
 
     androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/DialogNavigatorTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/DialogNavigatorTest.kt
index adab446..0b73ece 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/DialogNavigatorTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/DialogNavigatorTest.kt
@@ -20,6 +20,7 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
+import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.viewmodel.compose.viewModel
 import androidx.navigation.NavHostController
 import androidx.navigation.testing.TestNavigatorState
@@ -109,4 +110,154 @@
         rule.waitForIdle()
         assertThat(navController.currentDestination?.route).isEqualTo("first")
     }
+
+    @Test
+    fun testDialogMarkedTransitionComplete() {
+        lateinit var navController: NavHostController
+
+        rule.setContent {
+            navController = rememberNavController()
+            NavHost(navController, "first") {
+                composable("first") { }
+                dialog("second") {
+                    Text(defaultText)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            navController.navigate("second")
+            navController.navigate("second")
+        }
+
+        rule.waitForIdle()
+        val dialogNavigator = navController.navigatorProvider.getNavigator(
+            DialogNavigator::class.java
+        )
+        val bottomDialog = dialogNavigator.backStack.value[0]
+        val topDialog = dialogNavigator.backStack.value[1]
+
+        assertThat(bottomDialog.destination.route).isEqualTo("second")
+        assertThat(topDialog.destination.route).isEqualTo("second")
+        assertThat(topDialog).isNotEqualTo(bottomDialog)
+
+        assertThat(topDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.RESUMED
+        )
+        assertThat(bottomDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.STARTED
+        )
+
+        rule.runOnUiThread {
+            dialogNavigator.dismiss(topDialog)
+        }
+        rule.waitForIdle()
+
+        assertThat(topDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.DESTROYED
+        )
+        assertThat(bottomDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.RESUMED
+        )
+    }
+
+    @Test
+    fun testDialogMarkedTransitionCompleteInOrder() {
+        lateinit var navController: NavHostController
+
+        rule.setContent {
+            navController = rememberNavController()
+            NavHost(navController, "first") {
+                composable("first") { }
+                dialog("second") {
+                    Text(defaultText)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            navController.navigate("second")
+            navController.navigate("second")
+            navController.navigate("second")
+        }
+
+        rule.waitForIdle()
+        val dialogNavigator = navController.navigatorProvider.getNavigator(
+            DialogNavigator::class.java
+        )
+        val bottomDialog = dialogNavigator.backStack.value[0]
+        val middleDialog = dialogNavigator.backStack.value[1]
+        val topDialog = dialogNavigator.backStack.value[2]
+
+        assertThat(topDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.RESUMED
+        )
+        assertThat(middleDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.STARTED
+        )
+        assertThat(bottomDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.STARTED
+        )
+
+        rule.runOnUiThread {
+            dialogNavigator.dismiss(topDialog)
+        }
+        rule.waitForIdle()
+
+        assertThat(topDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.DESTROYED
+        )
+        assertThat(middleDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.RESUMED
+        )
+        assertThat(bottomDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.STARTED
+        )
+
+        rule.runOnUiThread {
+            dialogNavigator.dismiss(middleDialog)
+        }
+        rule.waitForIdle()
+
+        assertThat(middleDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.DESTROYED
+        )
+        assertThat(bottomDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.RESUMED
+        )
+    }
+
+    @Test
+    fun testDialogNavigateConsecutively() {
+        lateinit var navController: NavHostController
+
+        rule.setContent {
+            navController = rememberNavController()
+            NavHost(navController, "first") {
+                composable("first") { }
+                dialog("second") {
+                    Text(defaultText)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            navController.navigate("second")
+            navController.navigate("second")
+        }
+
+        rule.waitForIdle()
+        val dialogNavigator = navController.navigatorProvider.getNavigator(
+            DialogNavigator::class.java
+        )
+        val bottomDialog = dialogNavigator.backStack.value[0]
+        val topDialog = dialogNavigator.backStack.value[1]
+
+        assertThat(bottomDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.STARTED
+        )
+        assertThat(topDialog.lifecycle.currentState).isEqualTo(
+            Lifecycle.State.RESUMED
+        )
+    }
 }
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
index 9de4e8f..ea3b190 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
@@ -43,7 +43,7 @@
      * Dismiss the dialog destination associated with the given [backStackEntry].
      */
     internal fun dismiss(backStackEntry: NavBackStackEntry) {
-        state.popWithTransition(backStackEntry, false)
+        popBackStack(backStackEntry, false)
     }
 
     override fun navigate(
@@ -62,6 +62,13 @@
 
     override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
         state.popWithTransition(popUpTo, savedState)
+        // When popping, the incoming dialog is marked transitioning to hold it in
+        // STARTED. With pop complete, we can remove it from transition so it can move to RESUMED.
+        val popIndex = state.transitionsInProgress.value.indexOf(popUpTo)
+        // do not mark complete for entries up to and including popUpTo
+        state.transitionsInProgress.value.forEachIndexed { index, entry ->
+            if (index > popIndex) onTransitionComplete(entry)
+        }
     }
 
     internal fun onTransitionComplete(entry: NavBackStackEntry) {
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 265df05..59fd673 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -26,8 +26,8 @@
 dependencies {
     api(project(":navigation:navigation-common"))
     api("androidx.activity:activity-ktx:1.7.1")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
     api("androidx.annotation:annotation-experimental:1.1.0")
     implementation('androidx.collection:collection:1.0.0')
 
diff --git a/paging/paging-compose/src/commonMain/kotlin/androidx/paging/compose/LazyPagingItems.kt b/paging/paging-compose/src/commonMain/kotlin/androidx/paging/compose/LazyPagingItems.kt
index a9a1c63..8b2840c 100644
--- a/paging/paging-compose/src/commonMain/kotlin/androidx/paging/compose/LazyPagingItems.kt
+++ b/paging/paging-compose/src/commonMain/kotlin/androidx/paging/compose/LazyPagingItems.kt
@@ -23,6 +23,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.AndroidUiDispatcher
 import androidx.paging.CombinedLoadStates
 import androidx.paging.DifferCallback
 import androidx.paging.ItemSnapshotList
@@ -33,7 +34,6 @@
 import androidx.paging.PagingDataDiffer
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.collectLatest
@@ -60,7 +60,7 @@
      */
     private val flow: Flow<PagingData<T>>
 ) {
-    private val mainDispatcher = Dispatchers.Main
+    private val mainDispatcher = AndroidUiDispatcher.Main
 
     private val differCallback: DifferCallback = object : DifferCallback {
         override fun onChanged(position: Int, count: Int) {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index abc5297..e45d496 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -81,9 +81,10 @@
 
         mSandboxedSdkView2 = SandboxedSdkView(this@MainActivity)
         mSandboxedSdkView2.addStateChangedListener(StateChangeListener(mSandboxedSdkView2))
-        mSandboxedSdkView2.layoutParams = ViewGroup.LayoutParams(400, 400)
+        mSandboxedSdkView2.layoutParams = findViewById<LinearLayout>(
+            R.id.bottom_banner_container).layoutParams
         runOnUiThread {
-            findViewById<LinearLayout>(R.id.ad_layout).addView(mSandboxedSdkView2)
+            findViewById<LinearLayout>(R.id.bottom_banner_container).addView(mSandboxedSdkView2)
         }
         mSandboxedSdkView2.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(
             sdkApi.loadAd(/*isWebView=*/ false, "Hey!")
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
index dda74f2..1051e95 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -15,17 +15,20 @@
   limitations under the License.
   -->
 
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:weightSum="5"
+    android:orientation="vertical"
     tools:context=".MainActivity">
 
     <ScrollView
         android:id="@+id/scroll_view"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_weight="4"
+        android:layout_height="0dp"
         android:orientation="vertical">
         <LinearLayout
             android:id="@+id/ad_layout"
@@ -80,4 +83,10 @@
                 android:text="@string/long_text" />
         </LinearLayout>
     </ScrollView>
-</androidx.constraintlayout.widget.ConstraintLayout>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:id="@+id/bottom_banner_container"
+        android:orientation="vertical" />
+</androidx.appcompat.widget.LinearLayoutCompat>
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
index fd13072..4e5269b 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.pm.ActivityInfo
 import android.content.res.Configuration
+import android.graphics.Rect
 import android.os.Build
 import android.os.IBinder
 import android.view.SurfaceView
@@ -27,11 +28,16 @@
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
 import android.widget.LinearLayout
+import android.widget.ScrollView
 import androidx.annotation.RequiresApi
 import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState
 import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener
 import androidx.privacysandbox.ui.client.view.SandboxedSdkView
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -183,7 +189,7 @@
         Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
         context = InstrumentationRegistry.getInstrumentation().targetContext
         activity = activityScenarioRule.withActivity { this }
-        view = SandboxedSdkView(context)
+        view = SandboxedSdkView(activity)
         stateChangedListener = StateChangedListener()
         view.addStateChangedListener(stateChangedListener)
 
@@ -462,6 +468,33 @@
         assertThat(testSandboxedUiAdapter.inputToken).isEqualTo(token)
     }
 
+    @Test
+    fun getBoundingParent_withoutScrollParent() {
+        addViewToLayout()
+        onView(withId(R.id.mainlayout)).check(matches(isDisplayed()))
+        val boundingRect = Rect()
+        assertThat(view.getBoundingParent(boundingRect)).isTrue()
+        val rootView: ViewGroup = activity.findViewById(android.R.id.content)
+        val rootRect = Rect()
+        rootView.getGlobalVisibleRect(rootRect)
+        assertThat(boundingRect).isEqualTo(rootRect)
+    }
+
+    @Test
+    fun getBoundingParent_withScrollParent() {
+        val scrollViewRect = Rect()
+        val scrollView = activity.findViewById<ScrollView>(R.id.scroll_view)
+        activity.runOnUiThread {
+            scrollView.visibility = View.VISIBLE
+            scrollView.addView(view)
+        }
+        onView(withId(R.id.scroll_view)).check(matches(isDisplayed()))
+        assertThat(scrollView.getGlobalVisibleRect(scrollViewRect)).isTrue()
+        val boundingRect = Rect()
+        assertThat(view.getBoundingParent(boundingRect)).isTrue()
+        assertThat(scrollViewRect).isEqualTo(boundingRect)
+    }
+
     /**
      * Ensures that ACTIVE will only be sent to registered state change listeners after the next
      * frame commit.
diff --git a/privacysandbox/ui/ui-client/src/androidTest/res/layout/activity_main.xml b/privacysandbox/ui/ui-client/src/androidTest/res/layout/activity_main.xml
index 9922ac7..71da52f 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/res/layout/activity_main.xml
+++ b/privacysandbox/ui/ui-client/src/androidTest/res/layout/activity_main.xml
@@ -19,4 +19,9 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:id="@+id/mainlayout">
+    <ScrollView
+        android:layout_width="500px"
+        android:layout_height="500px"
+        android:id="@+id/scroll_view"
+        android:visibility="gone" />
 </LinearLayout>
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
index bbf3694..9931f30 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
@@ -18,13 +18,22 @@
 
 import android.content.Context
 import android.content.res.Configuration
+import android.graphics.Rect
 import android.os.Build
 import android.os.IBinder
 import android.util.AttributeSet
+import android.view.SurfaceControl
+import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewParent
+import android.view.ViewTreeObserver
 import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState.Active
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState.Idle
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState.Loading
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import java.util.concurrent.CopyOnWriteArrayList
 import kotlin.math.min
@@ -95,6 +104,24 @@
         visibility = GONE
     }
 
+    // This will only be invoked when the content view has been set and the window is attached.
+    private val surfaceChangedCallback = object : SurfaceHolder.Callback {
+        override fun surfaceCreated(p0: SurfaceHolder) {
+            setClippingBounds(true)
+            viewTreeObserver.addOnGlobalLayoutListener(globalLayoutChangeListener)
+        }
+
+        override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
+        }
+
+        override fun surfaceDestroyed(p0: SurfaceHolder) {
+        }
+    }
+
+    // This will only be invoked when the content view has been set and the window is attached.
+    private val globalLayoutChangeListener =
+        ViewTreeObserver.OnGlobalLayoutListener { setClippingBounds() }
+
     private var adapter: SandboxedUiAdapter? = null
     private var client: Client? = null
     private var isZOrderOnTop = true
@@ -103,6 +130,7 @@
     private var requestedHeight = -1
     private var isTransitionGroupSet = false
     private var windowInputToken: IBinder? = null
+    private var currentClippingBounds = Rect()
     internal val stateListenerManager: StateListenerManager = StateListenerManager()
 
     /**
@@ -144,6 +172,60 @@
         checkClientOpenSession()
     }
 
+    internal fun setClippingBounds(forceUpdate: Boolean = false) {
+        checkNotNull(contentView)
+        check(isAttachedToWindow)
+
+        val updateRequired = getBoundingParent(currentClippingBounds) || forceUpdate
+        if (!updateRequired) {
+            return
+        }
+
+        val sv: SurfaceView = contentView as SurfaceView
+        val attachedSurfaceControl = checkNotNull(sv.rootSurfaceControl) {
+            "attachedSurfaceControl should be non-null if the window is attached"
+        }
+        val name = "clippingBounds-${System.currentTimeMillis()}"
+        val clippingBoundsSurfaceControl =
+            SurfaceControl.Builder().setName(name)
+                .build()
+        val reparentSurfaceControlTransaction = SurfaceControl.Transaction()
+            .reparent(sv.surfaceControl, clippingBoundsSurfaceControl)
+
+        val reparentClippingBoundsTransaction =
+            checkNotNull(
+                attachedSurfaceControl.buildReparentTransaction(clippingBoundsSurfaceControl)) {
+                "Reparent transaction should be non-null if the window is attached"
+            }
+        reparentClippingBoundsTransaction.setCrop(
+            clippingBoundsSurfaceControl, currentClippingBounds)
+        reparentClippingBoundsTransaction.setVisibility(
+            clippingBoundsSurfaceControl, true)
+        reparentSurfaceControlTransaction.merge(reparentClippingBoundsTransaction)
+        attachedSurfaceControl.applyTransactionOnDraw(reparentSurfaceControlTransaction)
+    }
+
+    /**
+     * Computes the window space coordinates for the bounding parent of this view, and stores the
+     * result in [rect].
+     *
+     * Returns true if the coordinates have changed, false otherwise.
+     */
+    @VisibleForTesting
+    internal fun getBoundingParent(rect: Rect): Boolean {
+        val prevBounds = Rect(rect)
+        var viewParent: ViewParent? = parent
+        while (viewParent != null && viewParent is View) {
+            val v = viewParent as View
+            if (v.isScrollContainer || v.id == android.R.id.content) {
+                v.getGlobalVisibleRect(rect)
+                return prevBounds != rect
+            }
+            viewParent = viewParent.getParent()
+        }
+        return false
+    }
+
     private fun checkClientOpenSession() {
         val adapter = adapter
         if (client == null && adapter != null && windowInputToken != null &&
@@ -197,11 +279,17 @@
     }
 
     private fun removeContentView() {
+        removeCallbacks()
         if (childCount == 1) {
             super.removeViewAt(0)
         }
     }
 
+    private fun removeCallbacks() {
+        (contentView as? SurfaceView)?.holder?.removeCallback(surfaceChangedCallback)
+        viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutChangeListener)
+    }
+
     internal fun setContentView(contentView: View) {
         if (childCount > 1) {
             throw IllegalStateException("Number of children views must not exceed 1")
@@ -220,6 +308,10 @@
             stateListenerManager.currentUiSessionState =
                 SandboxedSdkUiSessionState.Active
         }
+
+        if (contentView is SurfaceView) {
+            contentView.holder.addCallback(surfaceChangedCallback)
+        }
     }
 
     internal fun onClientClosedSession(error: Throwable? = null) {
@@ -293,6 +385,7 @@
         client?.close()
         client = null
         windowInputToken = null
+        removeCallbacks()
         super.onDetachedFromWindow()
     }
 
diff --git a/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/test/BinderAdapterDelegateTest.kt b/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/test/BinderAdapterDelegateTest.kt
index f2bb448..5b8aaab 100644
--- a/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/test/BinderAdapterDelegateTest.kt
+++ b/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/test/BinderAdapterDelegateTest.kt
@@ -93,15 +93,15 @@
     }
 
     @Test
-    fun touchFocusTransferredForSwipeLeft() {
+    fun touchFocusNotTransferredForSwipeLeft() {
         onView(withId(R.id.surface_view)).perform(swipeLeft())
-        assertThat(transferTouchFocusLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(transferTouchFocusLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
     }
 
     @Test
-    fun touchFocusTransferredForSlowSwipeLeft() {
+    fun touchFocusNotTransferredForSlowSwipeLeft() {
         onView(withId(R.id.surface_view)).perform(slowSwipeLeft())
-        assertThat(transferTouchFocusLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue()
+        assertThat(transferTouchFocusLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isFalse()
     }
 
     @Test
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/TouchFocusTransferringView.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/TouchFocusTransferringView.kt
index 1ef330a..132f6e3 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/TouchFocusTransferringView.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/TouchFocusTransferringView.kt
@@ -25,11 +25,12 @@
 import android.widget.FrameLayout
 import androidx.annotation.RequiresApi
 import androidx.core.view.GestureDetectorCompat
+import kotlin.math.abs
 
 /**
  * A container [ViewGroup] that delegates touch events to the host or the UI provider.
  *
- * Touch events will first be passed to a scroll detector. If a scroll or fling
+ * Touch events will first be passed to a scroll detector. If a vertical scroll or fling
  * is detected, the gesture will be transferred to the host. Otherwise, the touch event will pass
  * through and be handled by the provider of UI.
  *
@@ -61,7 +62,7 @@
     /**
      * Handles intercepted touch events before they reach the UI provider.
      *
-     * If a scroll or fling event is caught, this is indicated by the [isScrolling] var.
+     * If a vertical scroll or fling event is caught, this is indicated by the [isScrolling] var.
      */
     private class ScrollDetector(context: Context) : GestureDetector.SimpleOnGestureListener() {
 
@@ -71,8 +72,12 @@
         private val gestureDetector: GestureDetectorCompat = GestureDetectorCompat(context, this)
 
         override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean {
-            isScrolling = true
-            return false
+            // A scroll is vertical if its y displacement is greater than its x displacement.
+            if (abs(dY) > abs(dX)) {
+                isScrolling = true
+                return false
+            }
+            return true
         }
 
         override fun onFling(
@@ -81,8 +86,12 @@
             velocityX: Float,
             velocityY: Float
         ): Boolean {
-            isScrolling = true
-            return false
+            // A fling is vertical if its y velocity is greater than its x velocity.
+            if (abs(velocityY) > abs(velocityX)) {
+                isScrolling = true
+                return false
+            }
+            return true
         }
 
         fun onTouchEvent(ev: MotionEvent) {
diff --git a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
index 8afa6de..c671c5a 100644
--- a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
+++ b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
@@ -87,6 +87,9 @@
         <receiver android:name="androidx.mediarouter.media.MediaTransferReceiver"
             android:exported="true" />
 
+        <receiver android:name="androidx.mediarouter.media.SystemRoutingUsingMediaRouter2Receiver"
+            android:exported="true" />
+
         <service
             android:name=".services.SampleMediaRouteProviderService"
             android:exported="true"
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 032cd8a..c4276b5 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -754,8 +754,12 @@
                     bounds, swipeDirection, segment, speed, getDisplayId()).pause(250);
 
             // Perform the gesture and return early if we reached the end
-            if (mGestureController.performGestureAndWait(
-                    Until.scrollFinished(direction), SCROLL_TIMEOUT, swipe)) {
+            Boolean scrollFinishedResult = mGestureController.performGestureAndWait(
+                    Until.scrollFinished(direction), SCROLL_TIMEOUT, swipe);
+            if (!Boolean.FALSE.equals(scrollFinishedResult)) {
+                if (scrollFinishedResult == null) {
+                    Log.i(TAG, "No scroll event received after scroll.");
+                }
                 return false;
             }
         }
@@ -919,8 +923,12 @@
         // Perform the gesture and return true if we did not reach the end
         Log.d(TAG, String.format("Flinging %s (bounds=%s) at %dpx/s.",
                 direction.name().toLowerCase(), bounds, speed));
-        return !mGestureController.performGestureAndWait(
+        Boolean scrollFinishedResult = mGestureController.performGestureAndWait(
                 Until.scrollFinished(direction), FLING_TIMEOUT, swipe);
+        if (scrollFinishedResult == null) {
+            Log.i(TAG, "No scroll event received after fling.");
+        }
+        return Boolean.FALSE.equals(scrollFinishedResult);
     }
 
     /** Sets this object's text content if it is an editable field. */
diff --git a/transition/transition/build.gradle b/transition/transition/build.gradle
index a4bf478..165c99b 100644
--- a/transition/transition/build.gradle
+++ b/transition/transition/build.gradle
@@ -8,7 +8,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
-    api(project(":core:core"))
+    api("androidx.core:core:1.12.0")
     implementation("androidx.collection:collection:1.1.0")
     compileOnly("androidx.fragment:fragment:1.2.5")
     compileOnly("androidx.appcompat:appcompat:1.0.1")
diff --git a/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java b/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
index 0d7a373..e02c4b6 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
@@ -43,6 +43,7 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.testutils.PollingCheck;
 
 import org.junit.Test;
 
@@ -217,13 +218,52 @@
         }
     }
 
+    @LargeTest
+    @Test
+    public void interruptSlidePosition() throws Throwable {
+        final Slide slide = new Slide(Gravity.LEFT);
+        slide.setDuration(1000);
+        slide.setInterpolator(new LinearInterpolator());
 
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+        final View redSquare = spy(new View(rule.getActivity()));
+        rule.runOnUiThread(() -> {
+            mRoot.addView(new View(mRoot.getContext()), 100, mRoot.getHeight() / 2);
+
+            redSquare.setBackgroundColor(Color.RED);
+            mRoot.addView(redSquare, ViewGroup.LayoutParams.MATCH_PARENT, 100);
+            redSquare.setVisibility(View.INVISIBLE);
+        });
+
+        // now slide in
+        rule.runOnUiThread(() -> {
+            TransitionManager.beginDelayedTransition(mRoot, slide);
+            redSquare.setVisibility(View.VISIBLE);
+        });
+
+        final float[] redStartX = new float[1];
+        rule.runOnUiThread(() -> redStartX[0] = redSquare.getTranslationX());
+        PollingCheck.waitFor(1000, () -> redSquare.getTranslationX() > redStartX[0] * 0.5f);
+        assertEquals(0f, redSquare.getTranslationY(), 0f);
+        final int[] interruptedPosition = new int[2];
+        rule.runOnUiThread(() -> {
+            TransitionManager.beginDelayedTransition(mRoot, slide);
+            redSquare.getLocationOnScreen(interruptedPosition);
+            mRoot.removeView(redSquare);
+        });
+
+        rule.runOnUiThread(() -> {
+            int[] position = new int[2];
+            mRoot.getLocationOnScreen(position);
+            position[0] += redSquare.getLeft() + redSquare.getTranslationX();
+            position[1] += redSquare.getTop() + redSquare.getTranslationY();
+            assertEquals(interruptedPosition[1], position[1]);
+            assertTrue(position[0] <= interruptedPosition[0]);
+        });
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void seekingSlideOut() throws Throwable {
-        if (Build.VERSION.SDK_INT < 34) {
-            return; // only supported on U+
-        }
         final TransitionActivity activity = rule.getActivity();
         TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
 
@@ -298,12 +338,9 @@
         });
     }
 
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void seekingSlideIn() throws Throwable {
-        if (Build.VERSION.SDK_INT < 34) {
-            return; // only supported on U+
-        }
         final TransitionActivity activity = rule.getActivity();
         TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
 
@@ -384,12 +421,9 @@
     }
 
 
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void seekWithTranslation() throws Throwable {
-        if (Build.VERSION.SDK_INT < 34) {
-            return; // only supported on U+
-        }
         final TransitionActivity activity = rule.getActivity();
         TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
         View redSquare = new View(activity);
diff --git a/transition/transition/src/main/java/androidx/transition/Transition.java b/transition/transition/src/main/java/androidx/transition/Transition.java
index 9cdb387..529448e 100644
--- a/transition/transition/src/main/java/androidx/transition/Transition.java
+++ b/transition/transition/src/main/java/androidx/transition/Transition.java
@@ -527,7 +527,7 @@
      * Transition's progress. The Transition will begin without starting any of the
      * animations.
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @NonNull
     TransitionSeekController createSeekController() {
         mSeekController = new SeekController();
@@ -970,7 +970,7 @@
      * values. The duration is calculated. It also adds the animators to mCurrentAnimators so that
      * each animator can support seeking.
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     void prepareAnimatorsForSeeking() {
         ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
         // Now prepare every Animator that was previously created for this transition
@@ -2370,7 +2370,7 @@
      *                           than getTotalDurationMillis() to indicate that it is playing
      *                           backwards.
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     void setCurrentPlayTimeMillis(long playTimeMillis, long lastPlayTimeMillis) {
         long duration = getTotalDurationMillis();
         boolean isReversed = playTimeMillis < lastPlayTimeMillis;
@@ -2691,7 +2691,7 @@
     /**
      * Internal implementation of TransitionSeekController.
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     class SeekController extends TransitionListenerAdapter implements TransitionSeekController,
             DynamicAnimation.OnAnimationUpdateListener {
         // Animation calculations appear to work better with numbers that range greater than 1
diff --git a/transition/transition/src/main/java/androidx/transition/TransitionSet.java b/transition/transition/src/main/java/androidx/transition/TransitionSet.java
index d0ae436..6c52d23 100644
--- a/transition/transition/src/main/java/androidx/transition/TransitionSet.java
+++ b/transition/transition/src/main/java/androidx/transition/TransitionSet.java
@@ -528,7 +528,7 @@
         return false;
     }
 
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Override
     void prepareAnimatorsForSeeking() {
         mTotalDuration = 0;
@@ -572,7 +572,7 @@
         return mTransitions.size() - 1;
     }
 
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Override
     void setCurrentPlayTimeMillis(long playTimeMillis, long lastPlayTimeMillis) {
         long duration = getTotalDurationMillis();
diff --git a/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java b/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
index 114674c..f37bc91 100644
--- a/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
+++ b/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
@@ -60,10 +60,6 @@
             startX = startPosition[0] - viewPosX + terminalX;
             startY = startPosition[1] - viewPosY + terminalY;
         }
-        // Initial position is at translation startX, startY, so position is offset by that amount
-        int startPosX = viewPosX + Math.round(startX - terminalX);
-        int startPosY = viewPosY + Math.round(startY - terminalY);
-
         view.setTranslationX(startX);
         view.setTranslationY(startY);
         if (startX == endX && startY == endY) {
@@ -74,7 +70,7 @@
                 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, startY, endY));
 
         TransitionPositionListener listener = new TransitionPositionListener(view, values.view,
-                startPosX, startPosY, terminalX, terminalY);
+                terminalX, terminalY);
         transition.addListener(listener);
         anim.addListener(listener);
         anim.setInterpolator(interpolator);
@@ -91,10 +87,10 @@
         private float mPausedY;
         private final float mTerminalX;
         private final float mTerminalY;
-        private boolean mIsAnimationCancelCalled;
+        private boolean mIsTransitionCanceled;
 
         TransitionPositionListener(View movingView, View viewInHierarchy,
-                int startX, int startY, float terminalX, float terminalY) {
+                float terminalX, float terminalY) {
             mMovingView = movingView;
             mViewInHierarchy = viewInHierarchy;
             mTerminalX = terminalX;
@@ -107,8 +103,9 @@
 
         @Override
         public void onAnimationCancel(Animator animation) {
-            setInterruptedPosition();
-            mIsAnimationCancelCalled = true;
+            mIsTransitionCanceled = true;
+            mMovingView.setTranslationX(mTerminalX);
+            mMovingView.setTranslationY(mTerminalY);
         }
 
         @Override
@@ -117,6 +114,9 @@
 
         @Override
         public void onTransitionEnd(@NonNull Transition transition, boolean isReverse) {
+            if (!mIsTransitionCanceled) {
+                mViewInHierarchy.setTag(R.id.transition_position, null);
+            }
             if (!isReverse) {
                 mMovingView.setTranslationX(mTerminalX);
                 mMovingView.setTranslationY(mTerminalY);
@@ -125,21 +125,19 @@
 
         @Override
         public void onTransitionEnd(@NonNull Transition transition) {
+            onTransitionEnd(transition, false);
         }
 
         @Override
         public void onTransitionCancel(@NonNull Transition transition) {
-            if (!mIsAnimationCancelCalled) {
-                setInterruptedPosition();
-            }
+            mIsTransitionCanceled = true;
             mMovingView.setTranslationX(mTerminalX);
             mMovingView.setTranslationY(mTerminalY);
-            int[] pos = new int[2];
-            mMovingView.getLocationOnScreen(pos);
         }
 
         @Override
         public void onTransitionPause(@NonNull Transition transition) {
+            setInterruptedPosition();
             mPausedX = mMovingView.getTranslationX();
             mPausedY = mMovingView.getTranslationY();
             mMovingView.setTranslationX(mTerminalX);
diff --git a/tv/integration-tests/playground/build.gradle b/tv/integration-tests/playground/build.gradle
index 4970492..0842d00 100644
--- a/tv/integration-tests/playground/build.gradle
+++ b/tv/integration-tests/playground/build.gradle
@@ -32,6 +32,8 @@
     implementation(project(":compose:material3:material3"))
     implementation(project(":navigation:navigation-compose"))
     implementation(project(":profileinstaller:profileinstaller"))
+    implementation(project(":compose:material:material-icons-core"))
+    implementation(project(":compose:material:material-icons-extended"))
 
     implementation(project(":tv:tv-foundation"))
     implementation(project(":tv:tv-material"))
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
index 22cb95a..1557ac8 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
@@ -32,8 +32,8 @@
 import androidx.compose.foundation.selection.selectableGroup
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.KeyboardArrowLeft
-import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
@@ -136,17 +136,15 @@
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(10.dp)
     ) {
-        @Suppress("DEPRECATION")
         NavigationItem(
-            imageVector = Icons.Default.KeyboardArrowRight,
+            imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
             text = "LTR",
             drawerValue = drawerValue,
             selectedIndex = selectedIndex,
             index = 0
         )
-        @Suppress("DEPRECATION")
         NavigationItem(
-            imageVector = Icons.Default.KeyboardArrowLeft,
+            imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft,
             text = "RTL",
             drawerValue = drawerValue,
             selectedIndex = selectedIndex,
diff --git a/tv/integration-tests/presentation/build.gradle b/tv/integration-tests/presentation/build.gradle
index 5a9de93..1b3e68c 100644
--- a/tv/integration-tests/presentation/build.gradle
+++ b/tv/integration-tests/presentation/build.gradle
@@ -32,7 +32,8 @@
     implementation(project(":compose:material3:material3"))
     implementation(project(":navigation:navigation-runtime"))
     implementation(project(":profileinstaller:profileinstaller"))
-    implementation "androidx.compose.material:material-icons-extended:1.3.1"
+    implementation(project(":compose:material:material-icons-core"))
+    implementation(project(":compose:material:material-icons-extended"))
 
 //    implementation 'io.coil-kt:coil-compose:2.2.2'
 //    implementation 'com.google.code.gson:gson:2.8.9'
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
index 44d35f9..22f0edd 100644
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
@@ -31,7 +31,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.ArrowRight
+import androidx.compose.material.icons.automirrored.outlined.ArrowForward
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -86,7 +86,7 @@
                 @Suppress("DEPRECATION")
                 AppButton(
                     text = "Watch on YouTube",
-                    icon = Icons.Outlined.ArrowRight,
+                    icon = Icons.AutoMirrored.Outlined.ArrowForward,
                 )
             },
         )
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 6fc7ea7..1ea43bf 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -640,6 +640,40 @@
     property public final long selectedContentColor;
   }
 
+  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api 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);
+    method public float getCollapsedDrawerItemWidth();
+    method public float getContainerHeightOneLine();
+    method public float getContainerHeightTwoLine();
+    method public androidx.compose.animation.EnterTransition getContentAnimationEnter();
+    method public androidx.compose.animation.ExitTransition getContentAnimationExit();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Border getDefaultBorder();
+    method public float getExpandedDrawerItemWidth();
+    method public float getIconSize();
+    method public float getNavigationDrawerItemElevation();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getTrailingBadgeContainerColor();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getTrailingBadgeContentColor();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.TextStyle getTrailingBadgeTextStyle();
+    method public androidx.tv.material3.NavigationDrawerItemGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow, optional androidx.tv.material3.Glow selectedGlow, optional androidx.tv.material3.Glow focusedSelectedGlow, optional androidx.tv.material3.Glow pressedSelectedGlow);
+    method public androidx.tv.material3.NavigationDrawerItemScale scale(optional @FloatRange(from=0.0) float scale, optional @FloatRange(from=0.0) float focusedScale, optional @FloatRange(from=0.0) float pressedScale, optional @FloatRange(from=0.0) float selectedScale, optional @FloatRange(from=0.0) float disabledScale, optional @FloatRange(from=0.0) float focusedSelectedScale, optional @FloatRange(from=0.0) float focusedDisabledScale, optional @FloatRange(from=0.0) float pressedSelectedScale);
+    method public androidx.tv.material3.NavigationDrawerItemShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape selectedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedSelectedShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape, optional androidx.compose.ui.graphics.Shape pressedSelectedShape);
+    property public final float CollapsedDrawerItemWidth;
+    property public final float ContainerHeightOneLine;
+    property public final float ContainerHeightTwoLine;
+    property public final androidx.compose.animation.EnterTransition ContentAnimationEnter;
+    property public final androidx.compose.animation.ExitTransition ContentAnimationExit;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Border DefaultBorder;
+    property public final float ExpandedDrawerItemWidth;
+    property public final float IconSize;
+    property public final float NavigationDrawerItemElevation;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long TrailingBadgeContainerColor;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long TrailingBadgeContentColor;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.ui.text.TextStyle TrailingBadgeTextStyle;
+    field public static final androidx.tv.material3.NavigationDrawerItemDefaults INSTANCE;
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api 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();
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 6fc7ea7..1ea43bf 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -640,6 +640,40 @@
     property public final long selectedContentColor;
   }
 
+  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api 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);
+    method public float getCollapsedDrawerItemWidth();
+    method public float getContainerHeightOneLine();
+    method public float getContainerHeightTwoLine();
+    method public androidx.compose.animation.EnterTransition getContentAnimationEnter();
+    method public androidx.compose.animation.ExitTransition getContentAnimationExit();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Border getDefaultBorder();
+    method public float getExpandedDrawerItemWidth();
+    method public float getIconSize();
+    method public float getNavigationDrawerItemElevation();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getTrailingBadgeContainerColor();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getTrailingBadgeContentColor();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.TextStyle getTrailingBadgeTextStyle();
+    method public androidx.tv.material3.NavigationDrawerItemGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow, optional androidx.tv.material3.Glow selectedGlow, optional androidx.tv.material3.Glow focusedSelectedGlow, optional androidx.tv.material3.Glow pressedSelectedGlow);
+    method public androidx.tv.material3.NavigationDrawerItemScale scale(optional @FloatRange(from=0.0) float scale, optional @FloatRange(from=0.0) float focusedScale, optional @FloatRange(from=0.0) float pressedScale, optional @FloatRange(from=0.0) float selectedScale, optional @FloatRange(from=0.0) float disabledScale, optional @FloatRange(from=0.0) float focusedSelectedScale, optional @FloatRange(from=0.0) float focusedDisabledScale, optional @FloatRange(from=0.0) float pressedSelectedScale);
+    method public androidx.tv.material3.NavigationDrawerItemShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape selectedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedSelectedShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape, optional androidx.compose.ui.graphics.Shape pressedSelectedShape);
+    property public final float CollapsedDrawerItemWidth;
+    property public final float ContainerHeightOneLine;
+    property public final float ContainerHeightTwoLine;
+    property public final androidx.compose.animation.EnterTransition ContentAnimationEnter;
+    property public final androidx.compose.animation.ExitTransition ContentAnimationExit;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Border DefaultBorder;
+    property public final float ExpandedDrawerItemWidth;
+    property public final float IconSize;
+    property public final float NavigationDrawerItemElevation;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long TrailingBadgeContainerColor;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long TrailingBadgeContentColor;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.ui.text.TextStyle TrailingBadgeTextStyle;
+    field public static final androidx.tv.material3.NavigationDrawerItemDefaults INSTANCE;
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api 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();
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 2edb6b7..7526ff4 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -43,6 +43,7 @@
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
     androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(project(":test:screenshot:screenshot"))
+    androidTestImplementation(project(":compose:material:material-icons-core"))
     androidTestImplementation(libs.testRunner)
 
     samples(project(":tv:tv-samples"))
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/DenseListItemScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/DenseListItemScreenshotTest.kt
index 5c64b89..245ee20 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/DenseListItemScreenshotTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/DenseListItemScreenshotTest.kt
@@ -24,8 +24,8 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
 import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.KeyboardArrowRight
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -371,9 +371,8 @@
                         )
                     },
                     trailingContent = {
-                        @Suppress("DEPRECATION")
                         Icon(
-                            imageVector = Icons.Filled.KeyboardArrowRight,
+                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
                             contentDescription = null,
                             modifier = Modifier.size(ListItemDefaults.IconSizeDense)
                         )
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ListItemScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ListItemScreenshotTest.kt
index 7948021..97c5ecf 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ListItemScreenshotTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ListItemScreenshotTest.kt
@@ -24,8 +24,8 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
 import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.KeyboardArrowRight
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -380,9 +380,8 @@
                         )
                     },
                     trailingContent = {
-                        @Suppress("DEPRECATION")
                         Icon(
-                            imageVector = Icons.Filled.KeyboardArrowRight,
+                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
                             contentDescription = null,
                             modifier = Modifier.size(ListItemDefaults.IconSize)
                         )
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
new file mode 100644
index 0000000..44de75e
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
@@ -0,0 +1,358 @@
+/*
+ * 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.material3
+
+import androidx.annotation.FloatRange
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideIn
+import androidx.compose.animation.slideOut
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.tokens.Elevation
+
+/**
+ * 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
+     */
+    val IconSize = 24.dp
+
+    /**
+     * The size of the NavigationDrawerItem when the drawer is collapsed
+     */
+    val CollapsedDrawerItemWidth = 56.dp
+
+    /**
+     * The size of the NavigationDrawerItem when the drawer is expanded
+     */
+    val ExpandedDrawerItemWidth = 256.dp
+
+    /**
+     * The default content padding [PaddingValues] used by NavigationDrawerItem when the drawer
+     * is expanded
+     */
+
+    val ContainerHeightOneLine = 56.dp
+    val ContainerHeightTwoLine = 64.dp
+
+    /**
+     * The default elevation used by NavigationDrawerItem
+     */
+    val NavigationDrawerItemElevation = Elevation.Level0
+
+    /**
+     * Animation enter default for inner content
+     */
+    val ContentAnimationEnter = fadeIn() + slideIn { IntOffset(-it.width, 0) }
+
+    /**
+     * Animation exit default for inner content
+     */
+    val ContentAnimationExit = fadeOut() + slideOut { IntOffset(0, 0) }
+
+    /**
+     * Default border used by NavigationDrawerItem
+     */
+    val DefaultBorder
+        @ReadOnlyComposable
+        @Composable get() = Border(
+            border = BorderStroke(
+                width = 2.dp,
+                color = MaterialTheme.colorScheme.border
+            ),
+        )
+
+    /**
+     * The default container color used by NavigationDrawerItem's trailing badge
+     */
+    val TrailingBadgeContainerColor
+        @ReadOnlyComposable
+        @Composable get() = MaterialTheme.colorScheme.tertiary
+
+    /**
+     * The default text style used by NavigationDrawerItem's trailing badge
+     */
+    val TrailingBadgeTextStyle
+        @ReadOnlyComposable
+        @Composable get() = MaterialTheme.typography.labelSmall
+
+    /**
+     * The default content color used by NavigationDrawerItem's trailing badge
+     */
+    val TrailingBadgeContentColor
+        @ReadOnlyComposable
+        @Composable get() = MaterialTheme.colorScheme.onTertiary
+
+    /**
+     * Creates a trailing badge for NavigationDrawerItem
+     */
+    @Composable
+    fun TrailingBadge(
+        text: String,
+        containerColor: Color = TrailingBadgeContainerColor,
+        contentColor: Color = TrailingBadgeContentColor
+    ) {
+        Box(
+            modifier = Modifier
+                .background(containerColor, RoundedCornerShape(50))
+                .padding(10.dp, 2.dp)
+        ) {
+            ProvideTextStyle(value = TrailingBadgeTextStyle) {
+                Text(
+                    text = text,
+                    color = contentColor,
+                )
+            }
+        }
+    }
+
+    /**
+     * Creates a [NavigationDrawerItemShape] that represents the default container shapes
+     * used in a selectable NavigationDrawerItem
+     *
+     * @param shape the default shape used when the NavigationDrawerItem is enabled
+     * @param focusedShape the shape used when the NavigationDrawerItem is enabled and focused
+     * @param pressedShape the shape used when the NavigationDrawerItem is enabled and pressed
+     * @param selectedShape the shape used when the NavigationDrawerItem is enabled and selected
+     * @param disabledShape the shape used when the NavigationDrawerItem is not enabled
+     * @param focusedSelectedShape the shape used when the NavigationDrawerItem is enabled,
+     * focused and selected
+     * @param focusedDisabledShape the shape used when the NavigationDrawerItem is not enabled
+     * and focused
+     * @param pressedSelectedShape the shape used when the NavigationDrawerItem is enabled,
+     * pressed and selected
+     */
+    fun shape(
+        shape: Shape = RoundedCornerShape(50),
+        focusedShape: Shape = shape,
+        pressedShape: Shape = shape,
+        selectedShape: Shape = shape,
+        disabledShape: Shape = shape,
+        focusedSelectedShape: Shape = shape,
+        focusedDisabledShape: Shape = disabledShape,
+        pressedSelectedShape: Shape = shape
+    ) = NavigationDrawerItemShape(
+        shape = shape,
+        focusedShape = focusedShape,
+        pressedShape = pressedShape,
+        selectedShape = selectedShape,
+        disabledShape = disabledShape,
+        focusedSelectedShape = focusedSelectedShape,
+        focusedDisabledShape = focusedDisabledShape,
+        pressedSelectedShape = pressedSelectedShape
+    )
+
+    /**
+     * Creates a [NavigationDrawerItemColors] that represents the default container &
+     * content colors used in a selectable NavigationDrawerItem
+     *
+     * @param containerColor the default container color used when the NavigationDrawerItem is
+     * enabled
+     * @param contentColor the default content color used when the NavigationDrawerItem is enabled
+     * @param inactiveContentColor the content color used when none of the navigation items have
+     * focus
+     * @param focusedContainerColor the container color used when the NavigationDrawerItem is
+     * enabled and focused
+     * @param focusedContentColor the content color used when the NavigationDrawerItem is enabled
+     * and focused
+     * @param pressedContainerColor the container color used when the NavigationDrawerItem is
+     * enabled and pressed
+     * @param pressedContentColor the content color used when the NavigationDrawerItem is enabled
+     * and pressed
+     * @param selectedContainerColor the container color used when the NavigationDrawerItem is
+     * enabled and selected
+     * @param selectedContentColor the content color used when the NavigationDrawerItem is
+     * enabled and selected
+     * @param disabledContainerColor the container color used when the NavigationDrawerItem is
+     * not enabled
+     * @param disabledContentColor the content color used when the NavigationDrawerItem is not
+     * enabled
+     * @param disabledInactiveContentColor the content color used when none of the navigation items
+     * have focus and this item is disabled
+     * @param focusedSelectedContainerColor the container color used when the
+     * NavigationDrawerItem is enabled, focused and selected
+     * @param focusedSelectedContentColor the content color used when the NavigationDrawerItem
+     * is enabled, focused and selected
+     * @param pressedSelectedContainerColor the container color used when the
+     * NavigationDrawerItem is enabled, pressed and selected
+     * @param pressedSelectedContentColor the content color used when the NavigationDrawerItem is
+     * enabled, pressed and selected
+     */
+    @ReadOnlyComposable
+    @Composable
+    fun colors(
+        containerColor: Color = Color.Transparent,
+        contentColor: Color = MaterialTheme.colorScheme.onSurface,
+        inactiveContentColor: Color = contentColor.copy(alpha = 0.4f),
+        focusedContainerColor: Color = MaterialTheme.colorScheme.inverseSurface,
+        focusedContentColor: Color = contentColorFor(focusedContainerColor),
+        pressedContainerColor: Color = focusedContainerColor,
+        pressedContentColor: Color = contentColorFor(focusedContainerColor),
+        selectedContainerColor: Color =
+            MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.4f),
+        selectedContentColor: Color = MaterialTheme.colorScheme.onSecondaryContainer,
+        disabledContainerColor: Color = Color.Transparent,
+        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface,
+        disabledInactiveContentColor: Color = disabledContentColor.copy(alpha = 0.4f),
+        focusedSelectedContainerColor: Color = focusedContainerColor,
+        focusedSelectedContentColor: Color = focusedContentColor,
+        pressedSelectedContainerColor: Color = pressedContainerColor,
+        pressedSelectedContentColor: Color = pressedContentColor
+    ) = NavigationDrawerItemColors(
+        containerColor = containerColor,
+        contentColor = contentColor,
+        inactiveContentColor = inactiveContentColor,
+        focusedContainerColor = focusedContainerColor,
+        focusedContentColor = focusedContentColor,
+        pressedContainerColor = pressedContainerColor,
+        pressedContentColor = pressedContentColor,
+        selectedContainerColor = selectedContainerColor,
+        selectedContentColor = selectedContentColor,
+        disabledContainerColor = disabledContainerColor,
+        disabledContentColor = disabledContentColor,
+        disabledInactiveContentColor = disabledInactiveContentColor,
+        focusedSelectedContainerColor = focusedSelectedContainerColor,
+        focusedSelectedContentColor = focusedSelectedContentColor,
+        pressedSelectedContainerColor = pressedSelectedContainerColor,
+        pressedSelectedContentColor = pressedSelectedContentColor
+    )
+
+    /**
+     * Creates a [NavigationDrawerItemScale] that represents the default scales used in a
+     * selectable NavigationDrawerItem
+     *
+     * scales are used to modify the size of a composable in different [Interaction] states
+     * e.g. `1f` (original) in default state, `1.2f` (scaled up) in focused state, `0.8f` (scaled
+     * down) in pressed state, etc.
+     *
+     * @param scale the scale used when the NavigationDrawerItem is enabled
+     * @param focusedScale the scale used when the NavigationDrawerItem is enabled and focused
+     * @param pressedScale the scale used when the NavigationDrawerItem is enabled and pressed
+     * @param selectedScale the scale used when the NavigationDrawerItem is enabled and selected
+     * @param disabledScale the scale used when the NavigationDrawerItem is not enabled
+     * @param focusedSelectedScale the scale used when the NavigationDrawerItem is enabled,
+     * focused and selected
+     * @param focusedDisabledScale the scale used when the NavigationDrawerItem is not enabled and
+     * focused
+     * @param pressedSelectedScale the scale used when the NavigationDrawerItem is enabled,
+     * pressed and selected
+     */
+    fun scale(
+        @FloatRange(from = 0.0) scale: Float = 1f,
+        @FloatRange(from = 0.0) focusedScale: Float = 1.05f,
+        @FloatRange(from = 0.0) pressedScale: Float = scale,
+        @FloatRange(from = 0.0) selectedScale: Float = scale,
+        @FloatRange(from = 0.0) disabledScale: Float = scale,
+        @FloatRange(from = 0.0) focusedSelectedScale: Float = focusedScale,
+        @FloatRange(from = 0.0) focusedDisabledScale: Float = disabledScale,
+        @FloatRange(from = 0.0) pressedSelectedScale: Float = scale
+    ) = NavigationDrawerItemScale(
+        scale = scale,
+        focusedScale = focusedScale,
+        pressedScale = pressedScale,
+        selectedScale = selectedScale,
+        disabledScale = disabledScale,
+        focusedSelectedScale = focusedSelectedScale,
+        focusedDisabledScale = focusedDisabledScale,
+        pressedSelectedScale = pressedSelectedScale
+    )
+
+    /**
+     * Creates a [NavigationDrawerItemBorder] that represents the default [Border]s
+     * applied on a selectable NavigationDrawerItem in different [Interaction] states
+     *
+     * @param border the default [Border] used when the NavigationDrawerItem is enabled
+     * @param focusedBorder the [Border] used when the NavigationDrawerItem is enabled and focused
+     * @param pressedBorder the [Border] used when the NavigationDrawerItem is enabled and pressed
+     * @param selectedBorder the [Border] used when the NavigationDrawerItem is enabled and
+     * selected
+     * @param disabledBorder the [Border] used when the NavigationDrawerItem is not enabled
+     * @param focusedSelectedBorder the [Border] used when the NavigationDrawerItem is enabled,
+     * focused and selected
+     * @param focusedDisabledBorder the [Border] used when the NavigationDrawerItem is not
+     * enabled and focused
+     * @param pressedSelectedBorder the [Border] used when the NavigationDrawerItem is enabled,
+     * pressed and selected
+     */
+    @ReadOnlyComposable
+    @Composable
+    fun border(
+        border: Border = Border.None,
+        focusedBorder: Border = border,
+        pressedBorder: Border = focusedBorder,
+        selectedBorder: Border = border,
+        disabledBorder: Border = border,
+        focusedSelectedBorder: Border = focusedBorder,
+        focusedDisabledBorder: Border = DefaultBorder,
+        pressedSelectedBorder: Border = border
+    ) = NavigationDrawerItemBorder(
+        border = border,
+        focusedBorder = focusedBorder,
+        pressedBorder = pressedBorder,
+        selectedBorder = selectedBorder,
+        disabledBorder = disabledBorder,
+        focusedSelectedBorder = focusedSelectedBorder,
+        focusedDisabledBorder = focusedDisabledBorder,
+        pressedSelectedBorder = pressedSelectedBorder
+    )
+
+    /**
+     * Creates a [NavigationDrawerItemGlow] that represents the default [Glow]s used in a
+     * selectable NavigationDrawerItem
+     *
+     * @param glow the [Glow] used when the NavigationDrawerItem is enabled, and has no other
+     * [Interaction]s
+     * @param focusedGlow the [Glow] used when the NavigationDrawerItem is enabled and focused
+     * @param pressedGlow the [Glow] used when the NavigationDrawerItem is enabled and pressed
+     * @param selectedGlow the [Glow] used when the NavigationDrawerItem is enabled and selected
+     * @param focusedSelectedGlow the [Glow] used when the NavigationDrawerItem is enabled,
+     * focused and selected
+     * @param pressedSelectedGlow the [Glow] used when the NavigationDrawerItem is enabled,
+     * pressed and selected
+     */
+    fun glow(
+        glow: Glow = Glow.None,
+        focusedGlow: Glow = glow,
+        pressedGlow: Glow = glow,
+        selectedGlow: Glow = glow,
+        focusedSelectedGlow: Glow = focusedGlow,
+        pressedSelectedGlow: Glow = glow
+    ) = NavigationDrawerItemGlow(
+        glow = glow,
+        focusedGlow = focusedGlow,
+        pressedGlow = pressedGlow,
+        selectedGlow = selectedGlow,
+        focusedSelectedGlow = focusedSelectedGlow,
+        pressedSelectedGlow = pressedSelectedGlow
+    )
+}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicCurvedTextTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicCurvedTextTest.kt
index 6ea9e3c..8611081 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicCurvedTextTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicCurvedTextTest.kt
@@ -49,7 +49,7 @@
 
         rule.runOnIdle {
             // TODO(b/219885899): Investigate why we need the extra passes.
-            assertEquals(CapturedInfo(2, 3, 2), capturedInfo)
+            assertEquals(CapturedInfo(2, 3, 1), capturedInfo)
         }
     }
 }
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
index c07beb5..3140de6 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.rememberCoroutineScope
@@ -138,6 +139,148 @@
     }
 
     @Test
+    fun onSwipe_whenNotAllowed_doesNotSwipe() {
+        lateinit var revealState: RevealState
+        rule.setContent {
+            revealState = rememberRevealState(
+                confirmValueChange = { revealValue ->
+                    revealValue != RevealValue.Revealing
+                }
+            )
+            swipeToRevealWithDefaults(state = revealState, modifier = Modifier.testTag(TEST_TAG))
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeLeft(startX = width / 2f, endX = 0f) }
+
+        rule.runOnIdle {
+            assertEquals(RevealValue.Covered, revealState.currentValue)
+        }
+    }
+
+    @Test
+    fun onMultiSwipe_whenNotAllowed_doesNotReset() {
+        lateinit var revealStateOne: RevealState
+        lateinit var revealStateTwo: RevealState
+        val testTagOne = "testTagOne"
+        val testTagTwo = "testTagTwo"
+        rule.setContent {
+            revealStateOne = rememberRevealState()
+            revealStateTwo = rememberRevealState(
+                confirmValueChange = { revealValue -> revealValue != RevealValue.Revealing }
+            )
+            Column {
+                swipeToRevealWithDefaults(
+                    state = revealStateOne,
+                    modifier = Modifier.testTag(testTagOne)
+                )
+                swipeToRevealWithDefaults(
+                    state = revealStateTwo,
+                    modifier = Modifier.testTag(testTagTwo)
+                )
+            }
+        }
+
+        // swipe the first S2R
+        rule.onNodeWithTag(testTagOne).performTouchInput {
+            swipeLeft(startX = width / 2f, endX = 0f)
+        }
+
+        // swipe the second S2R to a reveal value which is not allowed
+        rule.onNodeWithTag(testTagTwo).performTouchInput {
+            swipeLeft(startX = width / 2f, endX = 0f)
+        }
+
+        rule.runOnIdle {
+            assertEquals(RevealValue.Revealing, revealStateOne.currentValue)
+            assertEquals(RevealValue.Covered, revealStateTwo.currentValue)
+        }
+    }
+
+    @Test
+    fun onMultiSwipe_whenAllowed_resetsLastState() {
+        lateinit var revealStateOne: RevealState
+        lateinit var revealStateTwo: RevealState
+        val testTagOne = "testTagOne"
+        val testTagTwo = "testTagTwo"
+        rule.setContent {
+            revealStateOne = rememberRevealState()
+            revealStateTwo = rememberRevealState()
+            Column {
+                swipeToRevealWithDefaults(
+                    state = revealStateOne,
+                    modifier = Modifier.testTag(testTagOne)
+                )
+                swipeToRevealWithDefaults(
+                    state = revealStateTwo,
+                    modifier = Modifier.testTag(testTagTwo)
+                )
+            }
+        }
+
+        // swipe the first S2R
+        rule.onNodeWithTag(testTagOne).performTouchInput {
+            swipeLeft(startX = width / 2f, endX = 0f)
+        }
+
+        // swipe the second S2R to a reveal value
+        rule.onNodeWithTag(testTagTwo).performTouchInput {
+            swipeLeft(startX = width / 2f, endX = 0f)
+        }
+
+        rule.runOnIdle {
+            assertEquals(RevealValue.Covered, revealStateOne.currentValue)
+            assertEquals(RevealValue.Revealing, revealStateTwo.currentValue)
+        }
+    }
+
+    @Test
+    fun onSnapForDifferentStates_lastOneGetsReset() {
+        lateinit var revealStateOne: RevealState
+        lateinit var revealStateTwo: RevealState
+        rule.setContent {
+            revealStateOne = rememberRevealState()
+            revealStateTwo = rememberRevealState()
+            swipeToRevealWithDefaults(state = revealStateOne)
+            swipeToRevealWithDefaults(state = revealStateTwo)
+
+            val coroutineScope = rememberCoroutineScope()
+            coroutineScope.launch {
+                // First change
+                revealStateOne.snapTo(RevealValue.Revealing)
+                // Second change, in a different state
+                revealStateTwo.snapTo(RevealValue.Revealing)
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(RevealValue.Covered, revealStateOne.currentValue)
+        }
+    }
+
+    @Test
+    fun onMultiSnapOnSameState_doesNotReset() {
+        lateinit var revealStateOne: RevealState
+        lateinit var revealStateTwo: RevealState
+        val lastValue = RevealValue.Revealed
+        rule.setContent {
+            revealStateOne = rememberRevealState()
+            revealStateTwo = rememberRevealState()
+            swipeToRevealWithDefaults(state = revealStateOne)
+            swipeToRevealWithDefaults(state = revealStateTwo)
+
+            val coroutineScope = rememberCoroutineScope()
+            coroutineScope.launch {
+                revealStateOne.snapTo(RevealValue.Revealing) // First change
+                revealStateOne.snapTo(lastValue) // Second change, same state
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(lastValue, revealStateOne.currentValue)
+        }
+    }
+
+    @Test
     fun onSecondaryActionClick_setsLastClickAction() = verifyLastClickAction(
         expectedClickType = RevealActionType.SecondaryAction,
         initialRevealValue = RevealValue.Revealing,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
index 4a3185b..b2a3c52 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
@@ -45,6 +45,7 @@
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.AbsoluteAlignment
 import androidx.compose.ui.Alignment
@@ -54,8 +55,12 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
+import androidx.core.util.Predicate
+import java.util.concurrent.atomic.AtomicReference
 import kotlin.math.abs
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 /**
  * Short animation in milliseconds.
@@ -176,7 +181,8 @@
     animationSpec: AnimationSpec<Float>,
     confirmValueChange: (RevealValue) -> Boolean,
     positionalThreshold: Density.(totalDistance: Float) -> Float,
-    internal val anchors: Map<RevealValue, Float>
+    internal val anchors: Map<RevealValue, Float>,
+    internal val coroutineScope: CoroutineScope,
 ) {
     /**
      * [SwipeableV2State] internal instance for the state.
@@ -184,8 +190,13 @@
     internal val swipeableState = SwipeableV2State(
         initialValue = initialValue,
         animationSpec = animationSpec,
-        confirmValueChange = confirmValueChange,
-        positionalThreshold = positionalThreshold
+        confirmValueChange = { revealValue ->
+            confirmValueChangeAndReset(
+                confirmValueChange,
+                revealValue
+            )
+        },
+        positionalThreshold = positionalThreshold,
     )
 
     public var lastActionType by mutableStateOf(RevealActionType.None)
@@ -242,7 +253,13 @@
      *
      * @see Modifier.swipeableV2
      */
-    public suspend fun snapTo(targetValue: RevealValue) = swipeableState.snapTo(targetValue)
+    public suspend fun snapTo(targetValue: RevealValue) {
+        // Cover the previously open component if revealing a different one
+        if (targetValue != RevealValue.Covered) {
+            resetLastState(this)
+        }
+        swipeableState.snapTo(targetValue)
+    }
 
     /**
      * Animates to the [targetValue] with the animation spec provided.
@@ -250,7 +267,13 @@
      * @param targetValue The target [RevealValue] where the [currentValue] will animate
      * to.
      */
-    public suspend fun animateTo(targetValue: RevealValue) = swipeableState.animateTo(targetValue)
+    public suspend fun animateTo(targetValue: RevealValue) {
+        // Cover the previously open component if revealing a different one
+        if (targetValue != RevealValue.Covered) {
+            resetLastState(this)
+        }
+        swipeableState.animateTo(targetValue)
+    }
 
     /**
      * Require the current offset.
@@ -258,6 +281,41 @@
      * @throws IllegalStateException If the offset has not been initialized yet
      */
     internal fun requireOffset(): Float = swipeableState.requireOffset()
+
+    private fun confirmValueChangeAndReset(
+        confirmValueChange: Predicate<RevealValue>,
+        revealValue: RevealValue,
+    ): Boolean {
+        val canChangeValue = confirmValueChange.test(revealValue)
+        val currentState = this
+        // Update the state if the reveal value is changing to a different value than Covered.
+        if (canChangeValue &&
+            revealValue != RevealValue.Covered) {
+            coroutineScope.launch {
+                resetLastState(currentState)
+            }
+        }
+        return canChangeValue
+    }
+
+    /**
+     * Resets last state if a different SwipeToReveal is being moved to new anchor.
+     */
+    private suspend fun resetLastState(
+        currentState: RevealState
+    ) {
+        val oldState = SingleSwipeCoordinator.lastUpdatedState.getAndSet(currentState)
+        if (currentState != oldState) {
+            oldState?.animateTo(RevealValue.Covered)
+        }
+    }
+
+    /**
+     * A singleton instance to keep track of the [RevealState] which was modified the last time.
+     */
+    private object SingleSwipeCoordinator {
+        var lastUpdatedState: AtomicReference<RevealState?> = AtomicReference(null)
+    }
 }
 
 /**
@@ -284,13 +342,15 @@
         SwipeToRevealDefaults.defaultThreshold(),
     anchors: Map<RevealValue, Float> = createAnchors()
 ): RevealState {
+    val coroutineScope = rememberCoroutineScope()
     return remember(initialValue, animationSpec) {
         RevealState(
             initialValue = initialValue,
             animationSpec = animationSpec,
             confirmValueChange = confirmValueChange,
             positionalThreshold = positionalThreshold,
-            anchors = anchors
+            anchors = anchors,
+            coroutineScope = coroutineScope
         )
     }
 }
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToRevealSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToRevealSample.kt
new file mode 100644
index 0000000..e988234
--- /dev/null
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToRevealSample.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.wear.compose.material.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.rememberRevealState
+import androidx.wear.compose.material.AppCard
+import androidx.wear.compose.material.CardDefaults
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.ExperimentalWearMaterialApi
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.SwipeToRevealCard
+import androidx.wear.compose.material.SwipeToRevealChip
+import androidx.wear.compose.material.SwipeToRevealDefaults
+import androidx.wear.compose.material.Text
+
+@OptIn(ExperimentalWearMaterialApi::class, ExperimentalWearFoundationApi::class)
+@Composable
+@Sampled
+fun SwipeToRevealChipSample() {
+    SwipeToRevealChip(
+        revealState = rememberRevealState(),
+        modifier = Modifier.fillMaxWidth(),
+        primaryAction = SwipeToRevealDefaults.primaryAction(
+            icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+            label = { Text("Delete") },
+            onClick = { /* Add the click handler here */ }
+        ),
+        secondaryAction = SwipeToRevealDefaults.secondaryAction(
+            icon = { Icon(SwipeToRevealDefaults.MoreOptions, "More Options") },
+            onClick = { /* Add the click handler here */ }
+        ),
+        undoPrimaryAction = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") },
+            onClick = { /* Add the undo handler for primary action */ }
+        ),
+        undoSecondaryAction = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") },
+            onClick = { /* Add the undo handler for secondary action */ }
+        )
+    ) {
+        Chip(
+            onClick = { /* Add the chip click handler here */ },
+            colors = ChipDefaults.primaryChipColors(),
+            border = ChipDefaults.outlinedChipBorder()
+        ) {
+            Text("SwipeToReveal Chip")
+        }
+    }
+}
+
+@OptIn(ExperimentalWearMaterialApi::class, ExperimentalWearFoundationApi::class)
+@Composable
+@Sampled
+fun SwipeToRevealCardSample() {
+    SwipeToRevealCard(
+        revealState = rememberRevealState(),
+        modifier = Modifier.fillMaxWidth(),
+        primaryAction = SwipeToRevealDefaults.primaryAction(
+            icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+            label = { Text("Delete") },
+            onClick = { /* Add the click handler here */ }
+        ),
+        secondaryAction = SwipeToRevealDefaults.secondaryAction(
+            icon = { Icon(SwipeToRevealDefaults.MoreOptions, "More Options") },
+            onClick = { /* Add the click handler here */ }
+        ),
+        undoPrimaryAction = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") },
+            onClick = { /* Add the undo handler for primary action */ }
+        ),
+        undoSecondaryAction = SwipeToRevealDefaults.undoAction(
+            label = { Text("Undo") },
+            onClick = { /* Add the undo handler for secondary action */ }
+        )
+    ) {
+        AppCard(
+            onClick = { /* Add the Card click handler */ },
+            appName = { Text("AppName") },
+            appImage = {
+                Icon(
+                    painter = painterResource(id = R.drawable.ic_airplanemode_active_24px),
+                    contentDescription = "airplane",
+                    modifier = Modifier.size(CardDefaults.AppImageSize)
+                        .wrapContentSize(align = Alignment.Center),
+                )
+            },
+            title = { Text("AppCard") },
+            time = { Text("now") }
+        ) {
+            Text("Basic card with SwipeToReveal actions")
+        }
+    }
+}
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToReveal.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToReveal.kt
index 1d1e5ea..56ede21 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToReveal.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToReveal.kt
@@ -55,6 +55,9 @@
 /**
  * [SwipeToReveal] Material composable for Chips. This provides the default style for consistency.
  *
+ * Example of [SwipeToRevealChip] with primary and secondary actions
+ * @sample androidx.wear.compose.material.samples.SwipeToRevealChipSample
+ *
  * @param primaryAction A [SwipeToRevealAction] instance to describe the primary action when
  * swiping. See [SwipeToRevealDefaults.primaryAction]. The action will be triggered on click or a
  * full swipe.
@@ -105,6 +108,9 @@
 /**
  * [SwipeToReveal] Material composable for Cards. This provides the default style for consistency.
  *
+ * Example of [SwipeToRevealCard] with primary and secondary actions
+ * @sample androidx.wear.compose.material.samples.SwipeToRevealCardSample
+ *
  * @param primaryAction A [SwipeToRevealAction] instance to describe the primary action when
  * swiping. See [SwipeToRevealDefaults.primaryAction]. The action will be triggered on click or a
  * full swipe.
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index be867cc..e6d6c23 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -91,6 +91,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class CheckboxColors {
+    ctor public CheckboxColors(long checkedBoxColor, long checkedCheckmarkColor, long uncheckedBoxColor, long uncheckedCheckmarkColor, long disabledCheckedBoxColor, long disabledCheckedCheckmarkColor, long disabledUncheckedBoxColor, long disabledUncheckedCheckmarkColor);
     method public long getCheckedBoxColor();
     method public long getCheckedCheckmarkColor();
     method public long getDisabledCheckedBoxColor();
@@ -353,6 +354,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class RadioButtonColors {
+    ctor public RadioButtonColors(long selectedColor, long unselectedColor, long disabledSelectedColor, long disabledUnselectedColor);
     method public long getDisabledSelectedColor();
     method public long getDisabledUnselectedColor();
     method public long getSelectedColor();
@@ -431,6 +433,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SwitchColors {
+    ctor public SwitchColors(long checkedThumbColor, long checkedThumbIconColor, long checkedTrackColor, long checkedTrackBorderColor, long uncheckedThumbColor, long uncheckedThumbIconColor, long uncheckedTrackColor, long uncheckedTrackBorderColor, long disabledCheckedThumbColor, long disabledCheckedThumbIconColor, long disabledCheckedTrackColor, long disabledCheckedTrackBorderColor, long disabledUncheckedThumbColor, long disabledUncheckedThumbIconColor, long disabledUncheckedTrackColor, long disabledUncheckedTrackBorderColor);
     method public long getCheckedThumbColor();
     method public long getCheckedThumbIconColor();
     method public long getCheckedTrackBorderColor();
@@ -466,7 +469,7 @@
   }
 
   public final class SwitchDefaults {
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SwitchColors colors(optional long checkedThumbColor, optional long checkedThumbIconColor, optional long checkedTrackColor, optional long checkedTrackStrokeColor, optional long uncheckedThumbColor, optional long uncheckedThumbIconColor, optional long uncheckedTrackColor, optional long uncheckedTrackStrokeColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SwitchColors colors(optional long checkedThumbColor, optional long checkedThumbIconColor, optional long checkedTrackColor, optional long checkedTrackBorderColor, optional long uncheckedThumbColor, optional long uncheckedThumbIconColor, optional long uncheckedTrackColor, optional long uncheckedTrackBorderColor);
     field public static final androidx.wear.compose.material3.SwitchDefaults INSTANCE;
   }
 
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index be867cc..e6d6c23 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -91,6 +91,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class CheckboxColors {
+    ctor public CheckboxColors(long checkedBoxColor, long checkedCheckmarkColor, long uncheckedBoxColor, long uncheckedCheckmarkColor, long disabledCheckedBoxColor, long disabledCheckedCheckmarkColor, long disabledUncheckedBoxColor, long disabledUncheckedCheckmarkColor);
     method public long getCheckedBoxColor();
     method public long getCheckedCheckmarkColor();
     method public long getDisabledCheckedBoxColor();
@@ -353,6 +354,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class RadioButtonColors {
+    ctor public RadioButtonColors(long selectedColor, long unselectedColor, long disabledSelectedColor, long disabledUnselectedColor);
     method public long getDisabledSelectedColor();
     method public long getDisabledUnselectedColor();
     method public long getSelectedColor();
@@ -431,6 +433,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SwitchColors {
+    ctor public SwitchColors(long checkedThumbColor, long checkedThumbIconColor, long checkedTrackColor, long checkedTrackBorderColor, long uncheckedThumbColor, long uncheckedThumbIconColor, long uncheckedTrackColor, long uncheckedTrackBorderColor, long disabledCheckedThumbColor, long disabledCheckedThumbIconColor, long disabledCheckedTrackColor, long disabledCheckedTrackBorderColor, long disabledUncheckedThumbColor, long disabledUncheckedThumbIconColor, long disabledUncheckedTrackColor, long disabledUncheckedTrackBorderColor);
     method public long getCheckedThumbColor();
     method public long getCheckedThumbIconColor();
     method public long getCheckedTrackBorderColor();
@@ -466,7 +469,7 @@
   }
 
   public final class SwitchDefaults {
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SwitchColors colors(optional long checkedThumbColor, optional long checkedThumbIconColor, optional long checkedTrackColor, optional long checkedTrackStrokeColor, optional long uncheckedThumbColor, optional long uncheckedThumbIconColor, optional long uncheckedTrackColor, optional long uncheckedTrackStrokeColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SwitchColors colors(optional long checkedThumbColor, optional long checkedThumbIconColor, optional long checkedTrackColor, optional long checkedTrackBorderColor, optional long uncheckedThumbColor, optional long uncheckedThumbIconColor, optional long uncheckedTrackColor, optional long uncheckedTrackBorderColor);
     field public static final androidx.wear.compose.material3.SwitchDefaults INSTANCE;
   }
 
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
index f5728b5..9d77f8c 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
@@ -460,7 +460,7 @@
                     checkedThumbColor = thumbColor,
                     checkedThumbIconColor = thumbIconColor,
                     checkedTrackColor = trackColor,
-                    checkedTrackStrokeColor = trackStrokeColor
+                    checkedTrackBorderColor = trackStrokeColor
                 ),
                 modifier = Modifier.testTag(TEST_TAG)
             )
@@ -487,7 +487,7 @@
                     uncheckedThumbColor = thumbColor,
                     uncheckedThumbIconColor = thumbIconColor,
                     uncheckedTrackColor = trackColor,
-                    uncheckedTrackStrokeColor = trackStrokeColor
+                    uncheckedTrackBorderColor = trackStrokeColor
                 ),
                 modifier = Modifier.testTag(TEST_TAG)
             )
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SelectionControls.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SelectionControls.kt
index a3ef21c..0fe17a3 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SelectionControls.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SelectionControls.kt
@@ -230,13 +230,26 @@
 
 /**
  * Represents the content colors used in [Checkbox] in different states.
+ *
+ * @param checkedBoxColor The box color of [Checkbox] when enabled and checked.
+ * @param checkedCheckmarkColor The check mark color of [Checkbox] when enabled
+ * and checked.
+ * @param uncheckedBoxColor The box color of [Checkbox] when enabled and unchecked.
+ * @param uncheckedCheckmarkColor The check mark color of [Checkbox] when enabled
+ * and unchecked.
+ * @param disabledCheckedBoxColor The box color of [Checkbox] when disabled and checked.
+ * @param disabledCheckedCheckmarkColor The check mark color of [Checkbox] when disabled
+ * and checked.
+ * @param disabledUncheckedBoxColor The box color of [Checkbox] when disabled and unchecked.
+ * @param disabledUncheckedCheckmarkColor The check mark color of [Checkbox] when disabled
+ * and unchecked.
  */
 @Immutable
-class CheckboxColors internal constructor(
+class CheckboxColors(
     val checkedBoxColor: Color,
     val checkedCheckmarkColor: Color,
-    val uncheckedCheckmarkColor: Color,
     val uncheckedBoxColor: Color,
+    val uncheckedCheckmarkColor: Color,
     val disabledCheckedBoxColor: Color,
     val disabledCheckedCheckmarkColor: Color,
     val disabledUncheckedBoxColor: Color,
@@ -311,9 +324,29 @@
 
 /**
  * Represents the content colors used in [Switch] in different states.
+ *
+ * @param checkedThumbColor The thumb color of [Switch] when enabled and checked.
+ * @param checkedThumbIconColor The thumb icon color of [Switch] when enabled and checked.
+ * @param checkedTrackColor The track color of [Switch] when enabled and checked.
+ * @param checkedTrackBorderColor The track border color of [Switch] when enabled and checked.
+ * @param uncheckedThumbColor The thumb color of [Switch] when enabled and unchecked.
+ * @param uncheckedThumbIconColor The thumb icon color of [Switch] when enabled and unchecked.
+ * @param uncheckedTrackColor The track color of [Switch] when enabled and unchecked.
+ * @param uncheckedTrackBorderColor The track border color of [Switch] when enabled and unchecked.
+ * @param disabledCheckedThumbColor The thumb color of [Switch] when disabled and checked.
+ * @param disabledCheckedThumbIconColor The thumb icon color of [Switch] when disabled and checked.
+ * @param disabledCheckedTrackColor The track color of [Switch] when disabled and checked.
+ * @param disabledCheckedTrackBorderColor The track border color of [Switch] when disabled
+ * and checked.
+ * @param disabledUncheckedThumbColor The thumb color of [Switch] when disabled and unchecked.
+ * @param disabledUncheckedThumbIconColor The thumb icon color of [Switch] when disabled
+ * and unchecked.
+ * @param disabledUncheckedTrackColor The track color of [Switch] when disabled and unchecked.
+ * @param disabledUncheckedTrackBorderColor The track border color of [Switch] when disabled
+ * and unchecked.
  */
 @Immutable
-class SwitchColors internal constructor(
+class SwitchColors(
     val checkedThumbColor: Color,
     val checkedThumbIconColor: Color,
     val checkedTrackColor: Color,
@@ -426,9 +459,14 @@
 
 /**
  * Represents the content colors used in [RadioButton] in different states.
+ *
+ * @param selectedColor The color of the radio button when enabled and selected.
+ * @param unselectedColor The color of the radio button when enabled and unselected.
+ * @param disabledSelectedColor The color of the radio button when disabled and selected.
+ * @param disabledUnselectedColor The color of the radio button when disabled and unselected.
  */
 @Immutable
-class RadioButtonColors internal constructor(
+class RadioButtonColors(
     val selectedColor: Color,
     val unselectedColor: Color,
     val disabledSelectedColor: Color,
@@ -508,37 +546,37 @@
      * @param checkedThumbColor The thumb color of this [Switch] when enabled and checked.
      * @param checkedThumbIconColor The thumb icon color of this [Switch] when enabled and checked.
      * @param checkedTrackColor The track color of this [Switch] when enabled and checked.
-     * @param checkedTrackStrokeColor The track border color of this [Switch] when enabled and checked.
+     * @param checkedTrackBorderColor The track border color of this [Switch] when enabled and checked.
      * @param uncheckedThumbColor The thumb color of this [Switch] when enabled and unchecked.
      * @param uncheckedThumbIconColor The thumb icon color of this [Switch] when enabled and checked.
      * @param uncheckedTrackColor The track color of this [Switch] when enabled and unchecked.
-     * @param uncheckedTrackStrokeColor The track border color of this [Switch] when enabled and unchecked.
+     * @param uncheckedTrackBorderColor The track border color of this [Switch] when enabled and unchecked.
      */
     @Composable
     fun colors(
         checkedThumbColor: Color = MaterialTheme.colorScheme.onPrimary,
         checkedThumbIconColor: Color = MaterialTheme.colorScheme.primary,
         checkedTrackColor: Color = MaterialTheme.colorScheme.primaryDim,
-        checkedTrackStrokeColor: Color = MaterialTheme.colorScheme.primaryDim,
+        checkedTrackBorderColor: Color = MaterialTheme.colorScheme.primaryDim,
         uncheckedThumbColor: Color = MaterialTheme.colorScheme.outline,
         uncheckedThumbIconColor: Color = MaterialTheme.colorScheme.background,
         uncheckedTrackColor: Color = MaterialTheme.colorScheme.surface,
-        uncheckedTrackStrokeColor: Color = MaterialTheme.colorScheme.outline
+        uncheckedTrackBorderColor: Color = MaterialTheme.colorScheme.outline
     ): SwitchColors = SwitchColors(
         checkedThumbColor = checkedThumbColor,
         checkedThumbIconColor = checkedThumbIconColor,
         checkedTrackColor = checkedTrackColor,
-        checkedTrackBorderColor = checkedTrackStrokeColor,
+        checkedTrackBorderColor = checkedTrackBorderColor,
         uncheckedThumbColor = uncheckedThumbColor,
         uncheckedThumbIconColor = uncheckedThumbIconColor,
         uncheckedTrackColor = uncheckedTrackColor,
-        uncheckedTrackBorderColor = uncheckedTrackStrokeColor,
+        uncheckedTrackBorderColor = uncheckedTrackBorderColor,
         disabledCheckedThumbColor = checkedThumbColor.toDisabledColor(),
         disabledCheckedThumbIconColor = checkedThumbIconColor.toDisabledColor(),
         disabledCheckedTrackColor = checkedTrackColor.toDisabledColor(
             disabledAlpha = DisabledContainerAlpha
         ),
-        disabledCheckedTrackBorderColor = checkedTrackStrokeColor.toDisabledColor(
+        disabledCheckedTrackBorderColor = checkedTrackBorderColor.toDisabledColor(
             disabledAlpha = DisabledBorderAlpha
         ),
         disabledUncheckedThumbColor = uncheckedThumbColor.toDisabledColor(),
@@ -546,7 +584,7 @@
         disabledUncheckedTrackColor = uncheckedTrackColor.toDisabledColor(
             disabledAlpha = DisabledContainerAlpha
         ),
-        disabledUncheckedTrackBorderColor = uncheckedTrackStrokeColor.toDisabledColor(
+        disabledUncheckedTrackBorderColor = uncheckedTrackBorderColor.toDisabledColor(
             disabledAlpha = DisabledBorderAlpha
         )
     )
diff --git a/wear/compose/compose-ui-tooling/build.gradle b/wear/compose/compose-ui-tooling/build.gradle
index 67408bd..8c4de3f 100644
--- a/wear/compose/compose-ui-tooling/build.gradle
+++ b/wear/compose/compose-ui-tooling/build.gradle
@@ -28,7 +28,7 @@
 
     implementation(libs.kotlinStdlibCommon)
     implementation(project(":compose:ui:ui-tooling-preview"))
-    implementation(project(":wear:wear-tooling-preview"))
+    implementation("androidx.wear:wear-tooling-preview:1.0.0-alpha01")
 
     samples(project(":wear:compose:compose-material-samples"))
 }
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index c6f8d33..ee7dddf 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -41,6 +41,8 @@
 import androidx.wear.compose.foundation.samples.SwipeToRevealWithExpandables
 import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.material.samples.SwipeToRevealCardSample
+import androidx.wear.compose.material.samples.SwipeToRevealChipSample
 
 // Declare the swipe to dismiss demos so that we can use this variable as the background composable
 // for the SwipeToDismissDemo itself.
@@ -167,6 +169,12 @@
                 },
                 ComposableDemo("Swipe To Reveal - Undo") {
                     SwipeToRevealWithDifferentUndo()
+                },
+                ComposableDemo("Material S2R Chip") {
+                    SwipeToRevealChipSample()
+                },
+                ComposableDemo("Material S2R Card") {
+                    SwipeToRevealCardSample()
                 }
             )
         )
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index 5bd009f..a53ba3c 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -58,5 +58,4 @@
     // but it doesn't work in androidx.
     // See aosp/1804059
     androidTestImplementation "androidx.lifecycle:lifecycle-common-java8:2.4.0"
-    androidTestImplementation(project(":annotation:annotation"))
 }
\ No newline at end of file
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 463af2e..0fdf391 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
@@ -63,6 +63,8 @@
 import androidx.wear.protolayout.proto.TriggerProto.Trigger;
 import androidx.wear.protolayout.renderer.dynamicdata.NodeInfo.ResolvedAvd;
 
+import com.google.common.collect.ImmutableList;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -984,7 +986,7 @@
     /** Cancel any already running content transition animations. */
     @UiThread
     void cancelContentTransitionAnimations() {
-        mExitAnimations.forEach(QuotaAwareAnimationSet::cancelAnimations);
+        ImmutableList.copyOf(mExitAnimations).forEach(QuotaAwareAnimationSet::cancelAnimations);
         mExitAnimations.clear();
         mEnterAnimations.forEach(QuotaAwareAnimationSet::cancelAnimations);
         mEnterAnimations.clear();
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 07d873b..5ed004c 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -4136,6 +4136,58 @@
     }
 
     @Test
+    public void layoutGetsApplied_whenApplyingSecondMutation_beforeExitAnimationsAreFinished()
+            throws Exception {
+        Renderer renderer =
+                renderer(
+                        fingerprintedLayout(
+                                getTextElementWithExitAnimation("Hello", /* iterations= */ 10)));
+        mDataPipeline.setFullyVisible(true);
+        FrameLayout inflatedViewParent = renderer.inflate();
+        shadowOf(getMainLooper()).idle();
+        ShadowChoreographer.setPaused(true);
+        ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
+
+        ViewGroupMutation mutation =
+                renderer.computeMutation(
+                        getRenderedMetadata(inflatedViewParent),
+                        fingerprintedLayout(
+                                getTextElementWithExitAnimation("World", /* iterations= */ 10)));
+        ListenableFuture<Void> applyMutationFuture =
+                renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
+
+        shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
+        assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
+
+        ViewGroupMutation secondMutation =
+                renderer.computeMutation(
+                        getRenderedMetadata(inflatedViewParent),
+                        fingerprintedLayout(
+                                getTextElementWithExitAnimation(
+                                        "Second mutation",
+                                        /* iterations= */ 10)));
+
+        ListenableFuture<Void> applySecondMutationFuture =
+                renderer.mRenderer.applyMutation(inflatedViewParent, secondMutation);
+
+        // the previous mutation should be finished
+        assertThat(applyMutationFuture.isDone()).isTrue();
+        assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString())
+                .isEqualTo("World");
+
+        shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
+        assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
+
+        ShadowChoreographer.setPaused(false);
+        shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5));
+        applySecondMutationFuture.get();
+
+        assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
+        assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString())
+                .isEqualTo("Second mutation");
+    }
+
+    @Test
     public void slideInTransition_snapToOutside_startsFromOutsideParentBounds() throws Exception {
         Renderer renderer =
                 renderer(
diff --git a/wear/tiles/tiles-tooling-preview/build.gradle b/wear/tiles/tiles-tooling-preview/build.gradle
index 83e8392..d7d5e61 100644
--- a/wear/tiles/tiles-tooling-preview/build.gradle
+++ b/wear/tiles/tiles-tooling-preview/build.gradle
@@ -28,7 +28,7 @@
     implementation(project(":wear:protolayout:protolayout-proto"))
     implementation(project(":wear:tiles:tiles"))
 
-    api(project(":wear:wear-tooling-preview"))
+    api("androidx.wear:wear-tooling-preview:1.0.0-alpha01")
     api("androidx.annotation:annotation:1.6.0")
 }
 
diff --git a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
index aaa4a02..e8a1a9c 100644
--- a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
+++ b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
@@ -33,8 +33,6 @@
 import androidx.wear.tiles.tooling.preview.TilePreviewData
 import java.lang.reflect.Method
 import kotlin.math.roundToInt
-import kotlinx.coroutines.guava.await
-import kotlinx.coroutines.runBlocking
 
 private const val TOOLS_NS_URI = "http://schemas.android.com/tools"
 
@@ -64,6 +62,9 @@
  */
 internal class TileServiceViewAdapter(context: Context, attrs: AttributeSet) :
     FrameLayout(context, attrs) {
+
+    private val executor = ContextCompat.getMainExecutor(context)
+
     init {
         init(attrs)
     }
@@ -78,7 +79,7 @@
     internal fun init(tilePreviewMethodFqn: String) {
         val tilePreview = getTilePreview(tilePreviewMethodFqn)
         lateinit var tileRenderer: TileRenderer
-        tileRenderer = TileRenderer(context, ContextCompat.getMainExecutor(context)) { newState ->
+        tileRenderer = TileRenderer(context, executor) { newState ->
             tileRenderer.previewTile(tilePreview, newState)
         }
         tileRenderer.previewTile(tilePreview)
@@ -108,11 +109,12 @@
             .build()
         val resources = tilePreview.onTileResourceRequest(resourcesRequest, context)
 
-        runBlocking {
-            inflateAsync(layout, resources, this@TileServiceViewAdapter)
-                .await()
-                ?.apply { (layoutParams as LayoutParams).gravity = Gravity.CENTER }
-        }
+        val inflateFuture = inflateAsync(layout, resources, this@TileServiceViewAdapter)
+        inflateFuture.addListener({
+            inflateFuture.get()?.let {
+                (it.layoutParams as LayoutParams).gravity = Gravity.CENTER
+            }
+        }, executor)
     }
 }
 
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index b9a4094..e4144ac 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -835,7 +835,7 @@
  * data in its manifest (NB the value is a comma separated list):
  * ```
  * <meta-data android:name="android.support.wearable.complications.SUPPORTED_TYPES"
- *    android:value="GOAL_PROGRESS"/>
+ *    android:value="RANGED_VALUE"/>
  * ```
  *
  * @property value The [Float] value of this complication which is >= [min] and <= [max] or equal to
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 3f77015..b2e238e 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -29,6 +29,7 @@
 import android.graphics.Picture
 import android.graphics.PixelFormat
 import android.graphics.Rect
+import android.hardware.display.DisplayManager
 import android.os.Build
 import android.os.Bundle
 import android.os.Handler
@@ -36,6 +37,7 @@
 import android.provider.Settings
 import android.support.wearable.watchface.SharedMemoryImage
 import android.support.wearable.watchface.WatchFaceStyle
+import android.view.Display
 import android.view.Gravity
 import android.view.Surface
 import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
@@ -667,6 +669,32 @@
 
     internal val componentName = watchFaceHostApi.getComponentName()
 
+    init {
+        val context = watchFaceHostApi.getContext()
+        val displayManager =
+                context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+        displayManager.registerDisplayListener(
+                object : DisplayManager.DisplayListener {
+                    override fun onDisplayAdded(displayId: Int) {}
+
+                    override fun onDisplayChanged(displayId: Int) {
+                        val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!!
+                        if (display.state == Display.STATE_OFF &&
+                                watchState.isVisible.value == false) {
+                            // We want to avoid a glimpse of a stale time when transitioning from
+                            // hidden to visible, so we render two black frames to clear the buffers
+                            // when the display has been turned off and the watch is not visible.
+                            renderer.renderBlackFrame()
+                            renderer.renderBlackFrame()
+                        }
+                    }
+
+                    override fun onDisplayRemoved(displayId: Int) {}
+                },
+                watchFaceHostApi.getUiThreadHandler()
+        )
+    }
+
     internal fun getWatchFaceStyle() =
         WatchFaceStyle(
             componentName,
@@ -744,10 +772,6 @@
                 }
                 scheduleDraw()
             } else {
-                // We want to avoid a glimpse of a stale time when transitioning from hidden to
-                // visible, so we render two black frames to clear the buffers.
-                renderer.renderBlackFrame()
-                renderer.renderBlackFrame()
                 unregisterReceivers()
             }
         }
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
index dc199bd..e5b08aa 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
@@ -28,6 +28,7 @@
 import android.webkit.WebViewClient;
 import android.widget.Button;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.appcompat.app.AppCompatActivity;
@@ -37,15 +38,14 @@
 import androidx.webkit.WebViewCompat;
 import androidx.webkit.WebViewFeature;
 
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 
 /**
- * An {@link Activity} to exercise {@link WebViewCompat#addDocumentStartJavaScript(WebView, String,
- * Set)} related functionality.
+ * An {@link Activity} to exercise
+ * {@link WebViewCompat#addDocumentStartJavaScript(WebView, String, java.util.Set)} related
+ * functionality.
  */
-// TODO(swestphal): Remove the @SuppressLint after addDocumentStartJavaScript is unhidden.
-@SuppressLint("RestrictedApi")
 public class DocumentStartJavaScriptActivity extends AppCompatActivity {
     private final Uri mExampleUri = new Uri.Builder()
                                             .scheme("https")
@@ -54,7 +54,6 @@
                                             .appendPath("example")
                                             .appendPath("assets")
                                             .build();
-    private Button mReplyProxyButton;
 
     private static class MyWebViewClient extends WebViewClient {
         private final WebViewAssetLoader mAssetLoader;
@@ -88,9 +87,10 @@
         }
 
         @Override
-        public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
-                boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
-            if (message.getData().equals("initialization")) {
+        public void onPostMessage(@NonNull WebView view, WebMessageCompat message,
+                @NonNull Uri sourceOrigin,
+                boolean isMainFrame, @NonNull JavaScriptReplyProxy replyProxy) {
+            if ("initialization".equals(message.getData())) {
                 mReplyProxy = replyProxy;
             }
         }
@@ -117,16 +117,17 @@
                         .addPathHandler(mExampleUri.getPath() + "/", new AssetsPathHandler(this))
                         .build();
 
-        mReplyProxyButton = findViewById(R.id.button_reply_proxy);
+        Button replyProxyButton = findViewById(R.id.button_reply_proxy);
 
         WebView webView = findViewById(R.id.webview);
         webView.setWebViewClient(new MyWebViewClient(assetLoader));
         webView.getSettings().setJavaScriptEnabled(true);
 
-        HashSet<String> allowedOriginRules = new HashSet<>(Arrays.asList("https://example.com"));
+        HashSet<String> allowedOriginRules = new HashSet<>(
+                Collections.singletonList("https://example.com"));
         // Add WebMessageListeners.
         WebViewCompat.addWebMessageListener(webView, "replyObject", allowedOriginRules,
-                new ReplyMessageListener(mReplyProxyButton));
+                new ReplyMessageListener(replyProxyButton));
         final String jsCode = "replyObject.onmessage = function(event) {"
                 + "    document.getElementById('result').innerHTML = event.data;"
                 + "};"
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index 748c00e..b608615 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -67,6 +67,10 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
   }
 
+  public interface ScriptHandler {
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DOCUMENT_START_SCRIPT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public void remove();
+  }
+
   public abstract class ServiceWorkerClientCompat {
     ctor public ServiceWorkerClientCompat();
     method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
@@ -223,6 +227,7 @@
   }
 
   public class WebViewCompat {
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DOCUMENT_START_SCRIPT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ScriptHandler addDocumentStartJavaScript(android.webkit.WebView, String, java.util.Set<java.lang.String!>);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
     method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
@@ -257,6 +262,7 @@
     field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
     field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
     field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
+    field public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
     field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
     field public static final String FORCE_DARK = "FORCE_DARK";
     field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index 748c00e..b608615 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -67,6 +67,10 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
   }
 
+  public interface ScriptHandler {
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DOCUMENT_START_SCRIPT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public void remove();
+  }
+
   public abstract class ServiceWorkerClientCompat {
     ctor public ServiceWorkerClientCompat();
     method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
@@ -223,6 +227,7 @@
   }
 
   public class WebViewCompat {
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DOCUMENT_START_SCRIPT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ScriptHandler addDocumentStartJavaScript(android.webkit.WebView, String, java.util.Set<java.lang.String!>);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
     method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
@@ -257,6 +262,7 @@
     field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
     field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
     field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
+    field public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
     field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
     field public static final String FORCE_DARK = "FORCE_DARK";
     field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ScriptHandler.java b/webkit/webkit/src/main/java/androidx/webkit/ScriptHandler.java
index 3f45801..e69adc7 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ScriptHandler.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ScriptHandler.java
@@ -17,29 +17,19 @@
 package androidx.webkit;
 
 import androidx.annotation.RequiresFeature;
-import androidx.annotation.RestrictTo;
 
 /**
- * This class represents the return result from {@link WebViewCompat#addDocumentStartJavaScript(
- * android.webkit.WebView, String, Set)}. Call {@link ScriptHandler#remove()} when the
+ * This interface represents the return result from {@link WebViewCompat#addDocumentStartJavaScript(
+ * android.webkit.WebView, String, java.util.Set)}. Call {@link ScriptHandler#remove()} when the
  * corresponding JavaScript script should be removed.
  *
- * @see WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView, String, Set)
- *
- * TODO(swestphal): unhide when ready.
+ * @see WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView, String, java.util.Set)
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class ScriptHandler {
+public interface ScriptHandler {
     /**
      * Removes the corresponding script, it will take effect from next page load.
      */
     @RequiresFeature(name = WebViewFeature.DOCUMENT_START_SCRIPT,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-    public abstract void remove();
-
-    /**
-     * This class cannot be created by applications.
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    public ScriptHandler() {}
+    void remove();
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index a7c2377..34472aa 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -782,7 +782,7 @@
      * origin matches {@code allowedOriginRules} when the document begins to load.
      *
      * <p>Note that the script will run before any of the page's JavaScript code and the DOM tree
-     * might not be ready at this moment. It will block the loadng of the page until it's finished,
+     * might not be ready at this moment. It will block the loading of the page until it's finished,
      * so should be kept as short as possible.
      *
      * <p>The injected object from {@link #addWebMessageListener(WebView, String, Set,
@@ -809,13 +809,10 @@
      * @throws IllegalArgumentException If one of the {@code allowedOriginRules} is invalid.
      * @see #addWebMessageListener(WebView, String, Set, WebMessageListener)
      * @see ScriptHandler
-     * @deprecated unreleased API will be removed in 1.9.0
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @RequiresFeature(
             name = WebViewFeature.DOCUMENT_START_SCRIPT,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-    @Deprecated
     public static @NonNull ScriptHandler addDocumentStartJavaScript(
             @NonNull WebView webview,
             @NonNull String script,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 2a982dc..87f4b9e 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -472,10 +472,7 @@
      * Feature for {@link #isFeatureSupported(String)}.
      * This feature covers {@link WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,
      * String, Set)}.
-     *
-     * TODO(swestphal): unhide when ready.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
 
     /**
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
index e882b7b..87e7eed 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
@@ -27,14 +27,24 @@
 /**
  * Internal implementation of {@link androidx.webkit.ScriptHandler}.
  */
-public class ScriptHandlerImpl extends ScriptHandler {
-    private ScriptHandlerBoundaryInterface mBoundaryInterface;
+public class ScriptHandlerImpl implements ScriptHandler {
+    private final ScriptHandlerBoundaryInterface mBoundaryInterface;
 
     private ScriptHandlerImpl(@NonNull ScriptHandlerBoundaryInterface boundaryInterface) {
         mBoundaryInterface = boundaryInterface;
     }
 
     /**
+     * Removes the corresponding script from WebView.
+     */
+    @Override
+    public void remove() {
+        // If this method is called, the feature must exist, so no need to check feature
+        // DOCUMENT_START_JAVASCRIPT.
+        mBoundaryInterface.remove();
+    }
+
+    /**
      * Create an AndroidX ScriptHandler from the given InvocationHandler.
      */
     public static @NonNull ScriptHandlerImpl toScriptHandler(
@@ -44,14 +54,4 @@
                         ScriptHandlerBoundaryInterface.class, invocationHandler);
         return new ScriptHandlerImpl(boundaryInterface);
     }
-
-    /**
-     * Removes the corresponding script from WebView.
-     */
-    @Override
-    public void remove() {
-        // If this method is called, the feature must exist, so no need to check feature
-        // DOCUMENT_START_JAVASCRIPT.
-        mBoundaryInterface.remove();
-    }
 }
diff --git a/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt b/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt
index bc758ac..83b7cbb 100644
--- a/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt
+++ b/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt
@@ -23,10 +23,12 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -36,7 +38,7 @@
 
     @Test
     fun testDefaultNetworkConstraints() {
-        val request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java)
+        val request = UserInitiatedTaskRequest(MyTask::class.java)
         val networkRequest = NetworkRequest.Builder()
                                 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                                 .build()
@@ -50,7 +52,7 @@
 
     @Test
     fun testCustomNetworkConstraints() {
-        val request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        val request = UserInitiatedTaskRequest(MyTask::class.java,
             _constraints = Constraints(NetworkRequest.Builder()
                 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
                 .build()
@@ -70,16 +72,16 @@
     @Test
     fun testTags() {
         val taskClassName = "androidx.work.datatransfer.UserInitiatedTaskRequestTest\$MyTask"
-        var request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java)
+        var request = UserInitiatedTaskRequest(MyTask::class.java)
         assertEquals(1, request.tags.size)
         assertEquals(taskClassName, request.tags.get(0))
 
-        request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        request = UserInitiatedTaskRequest(MyTask::class.java,
                                            _tags = mutableListOf("test"))
         assertEquals(2, request.tags.size)
         assertTrue(request.tags.contains("test"))
 
-        request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        request = UserInitiatedTaskRequest(MyTask::class.java,
                                            _tags = mutableListOf("test", "test2"))
         assertEquals(3, request.tags.size)
         assertTrue(request.tags.contains(taskClassName))
@@ -89,29 +91,52 @@
 
     @Test
     fun testDefaultTransferInfo() {
-        val request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java)
+        val request = UserInitiatedTaskRequest(MyTask::class.java)
         assertNull(request.transferInfo)
     }
 
     @Test
     fun testCustomTransferInfo() {
-        var request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        var request = UserInitiatedTaskRequest(MyTask::class.java,
             _transferInfo = TransferInfo(estimatedDownloadBytes = 1000L))
         val transferInfo = TransferInfo(0L, 1000L)
         assertEquals(request.transferInfo, transferInfo)
 
-        request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        request = UserInitiatedTaskRequest(MyTask::class.java,
             _transferInfo = TransferInfo(estimatedUploadBytes = 1000L))
         val transferInfo2 = TransferInfo(1000L, 0L)
         assertEquals(request.transferInfo, transferInfo2)
         assertNotEquals(request.transferInfo, transferInfo)
 
-        request = UserInitiatedTaskRequest(MyTask::class.java, MyFgs::class.java,
+        request = UserInitiatedTaskRequest(MyTask::class.java,
             _transferInfo = TransferInfo(2000L, 20L))
         val transferInfo3 = TransferInfo(2000L, 20L)
         assertEquals(request.transferInfo, transferInfo3)
     }
 
+    @Test
+    fun testDefaultFallbackPolicy(): Unit = runBlocking {
+        // Default policy FALLBACK_NONE should allow enqueue
+        val request = UserInitiatedTaskRequest(MyTask::class.java)
+        request.enqueue(ApplicationProvider.getApplicationContext())
+    }
+
+    @Test
+    fun testCustomFallbackPolicy(): Unit = runBlocking {
+        val request = UserInitiatedTaskRequest(MyTask::class.java,
+            fallbackPolicy = UserInitiatedTaskRequest.FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE)
+        try {
+            request.enqueue(ApplicationProvider.getApplicationContext())
+            fail("Expected enqueue to fail without setting a foreground service")
+        } catch (_: IllegalArgumentException) {
+            // expected
+        }
+
+        request.setForegroundService(MyFgs::class.java,
+            UserInitiatedTaskRequest.ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_DETACH)
+        request.enqueue(ApplicationProvider.getApplicationContext())
+    }
+
     private class MyTask : UserInitiatedTask(
         "test_task",
         ApplicationProvider.getApplicationContext()
diff --git a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt
index 62aac55..3578843 100644
--- a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt
+++ b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt
@@ -25,20 +25,9 @@
 class UserInitiatedTaskRequest constructor(
     private val task: Class<out UserInitiatedTask>,
     /**
-     * The foreground service which will be used as a fallback solution on Android 14- devices.
-     *
-     * <p>
-     * Upon scheduling the task request, the library will call [Context.startForegroundService] with
-     * [ACTION_UIT_SCHEDULE] on the given service here.
-     * The app needs to call [android.app.Service.startForeground] within a certain amount of time,
-     * otherwise it will crash with a [android.app.ForegroundServiceDidNotStartInTimeException].
+     * [FallbackPolicy] indicating what the library should do on Android 14- devices.
      */
-    private val service: Class<out AbstractUitService>,
-    /**
-     * [ForegroundServiceOnTaskFinishPolicy] indicating what should occur when the task is finished.
-     */
-    private val onTaskFinishPolicy: ForegroundServiceOnTaskFinishPolicy =
-        ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_STOP_FOREGROUND,
+    private val fallbackPolicy: FallbackPolicy = FallbackPolicy.FALLBACK_NONE,
     /**
      * [Constraints] required for this task to run.
      * The default value assumes a requirement of any internet.
@@ -71,16 +60,56 @@
     val tags: List<String>
         get() = _tags
 
+    /**
+     * The foreground service which will be used as a fallback solution on Android 14- devices.
+     * This is only used if [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set.
+     */
+    var service: Class<out AbstractUitService>? = null
+
+    /**
+     * [ForegroundServiceOnTaskFinishPolicy] indicating what should occur when the task is finished.
+     */
+    var onTaskFinishPolicy: ForegroundServiceOnTaskFinishPolicy =
+        ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_STOP_FOREGROUND
+
     init {
         // Update the list of tags to include the UserInitiatedTask class name if available
         _tags += task.name
     }
 
+    /**
+     * Set the [AbstractUitService] service to fallback to on Android 14- devices along with
+     * a [ForegroundServiceOnTaskFinishPolicy] policy which defines what will happen when the
+     * task is finished.
+     *
+     * This is only used if [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set. If this method
+     * is not called and [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set, an exception will
+     * be thrown when the task is enqueued.
+     *
+     * Upon scheduling the task request, the library will call [Context.startForegroundService] with
+     * [ACTION_UIT_SCHEDULE] on the given service here.
+     * The app needs to call [android.app.Service.startForeground] within a certain amount of time,
+     * otherwise it will crash with a [android.app.ForegroundServiceDidNotStartInTimeException].
+     */
+    fun setForegroundService(
+        service: Class<out AbstractUitService>,
+        onTaskFinishPolicy: ForegroundServiceOnTaskFinishPolicy =
+            ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_STOP_FOREGROUND
+    ) {
+        this.service = service
+        this.onTaskFinishPolicy = onTaskFinishPolicy
+    }
+
     internal fun getTaskState(): TaskState {
         return TaskState.TASK_STATE_INVALID // TODO: update impl
     }
 
     suspend fun enqueue(@Suppress("UNUSED_PARAMETER") context: Context) {
+        if (this.fallbackPolicy == FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE &&
+            this.service == null) {
+            throw IllegalArgumentException("FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE is set," +
+                " but a foreground service has not been set via setForegroundService().")
+        }
         // TODO: update impl
     }
 
@@ -93,6 +122,26 @@
             "androidx.work.datatransfer.UserInitiatedTaskRequest.SCHEDULE"
     }
 
+    enum class FallbackPolicy {
+        /**
+         * Indicates to the library that it should not do anything on Android 14- devices. The
+         * developer will perform the data transfer task on previous versions of Android with their
+         * own logic.
+         *
+         * **This is the default policy.**
+         */
+        FALLBACK_NONE,
+
+        /**
+         * Indicates that the developer will provide an implementation of [AbstractUitService] which
+         * will be used by the library to perform the data transfer work on Android 14- devices.
+         *
+         * _The foreground service fallback will act as a best effort to perform the data transfer
+         * work on Android 14- devices._
+         */
+        FALLBACK_TO_FOREGROUND_SERVICE,
+    }
+
     enum class ForegroundServiceOnTaskFinishPolicy {
         /**
          * This indicates that the foreground service should be stopped when the job is done.