Merge "Add DynamicFragmentNavigatorDestinationBuilder constructor" into androidx-main
diff --git a/OWNERS b/OWNERS
index 9b9a018..c3f01db 100644
--- a/OWNERS
+++ b/OWNERS
@@ -34,6 +34,8 @@
 # WebKit
 per-file *libraryversions.toml = [email protected], [email protected]
 per-file *docs-public/build.gradle = [email protected], [email protected]
+# Compose *
+per-file *libraryversions.toml = [email protected]
 
 # Copybara can self-approve CLs within synced docs.
 per-file docs/** = [email protected]
\ No newline at end of file
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 730d9b9..2e41cd9 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -33,7 +33,7 @@
 dependencies {
 
     api(project(":activity:activity"))
-    api("androidx.core:core-ktx:1.9.0") {
+    api("androidx.core:core-ktx:1.13.0") {
         because "Mirror activity dependency graph for -ktx artifacts"
     }
     api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") {
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 4068049..4d1dfeb 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -25,7 +25,7 @@
 
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.collection:collection:1.0.0")
-    api(projectOrArtifact(":core:core"))
+    api("androidx.core:core:1.13.0")
     api("androidx.lifecycle:lifecycle-runtime:2.6.1")
     api("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
     api("androidx.savedstate:savedstate:1.2.1")
diff --git a/activity/integration-tests/testapp/build.gradle b/activity/integration-tests/testapp/build.gradle
index ce12c4a..09b6006 100644
--- a/activity/integration-tests/testapp/build.gradle
+++ b/activity/integration-tests/testapp/build.gradle
@@ -35,7 +35,7 @@
     implementation("androidx.core:core-splashscreen:1.0.0")
 
     // Manually align dependencies across debugRuntime and debugAndroidTestRuntime.
-    androidTestImplementation(project(":annotation:annotation"))
+    androidTestImplementation("androidx.annotation:annotation:1.6.0")
     androidTestImplementation("androidx.annotation:annotation-experimental:1.4.0")
 
     androidTestImplementation(libs.kotlinStdlib)
diff --git a/annotation/annotation/api/1.8.0-beta02.txt b/annotation/annotation/api/1.8.0-beta02.txt
new file mode 100644
index 0000000..dababc5
--- /dev/null
+++ b/annotation/annotation/api/1.8.0-beta02.txt
@@ -0,0 +1,372 @@
+// Signature format: 4.0
+package androidx.annotation {
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimatorRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnyRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface AnyThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ArrayRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AttrRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface BinderThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface BoolRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CallSuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CheckResult {
+    method public abstract String suggest() default "";
+    property public abstract String suggest;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ChecksSdkIntAtLeast {
+    method public abstract int api() default -1;
+    method public abstract String codename() default "";
+    method public abstract int extension() default 0;
+    method public abstract int lambda() default -1;
+    method public abstract int parameter() default -1;
+    property public abstract int api;
+    property public abstract String codename;
+    property public abstract int extension;
+    property public abstract int lambda;
+    property public abstract int parameter;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorInt {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorLong {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ColorRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CONSTRUCTOR) public @interface ContentView {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface DeprecatedSinceApi {
+    method public abstract int api();
+    method public abstract String message() default "";
+    property public abstract int api;
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DimenRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Dimension {
+    method public abstract int unit() default androidx.annotation.Dimension.PX;
+    property public abstract int unit;
+    field public static final androidx.annotation.Dimension.Companion Companion;
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  public static final class Dimension.Companion {
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public @interface Discouraged {
+    method public abstract String message();
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface DisplayContext {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface DoNotInline {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DrawableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface EmptySuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface FloatRange {
+    method public abstract double from() default kotlin.jvm.internal.DoubleCompanionObject.NEGATIVE_INFINITY;
+    method public abstract boolean fromInclusive() default true;
+    method public abstract double to() default kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY;
+    method public abstract boolean toInclusive() default true;
+    property public abstract double from;
+    property public abstract boolean fromInclusive;
+    property public abstract double to;
+    property public abstract boolean toInclusive;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FontRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FractionRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface GravityInt {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface GuardedBy {
+    method public abstract String value();
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface HalfFloat {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IdRes {
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InspectableProperty {
+    method @Deprecated public abstract int attributeId() default 0;
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping();
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping();
+    method @Deprecated public abstract boolean hasAttributeId() default true;
+    method @Deprecated public abstract String name() default "";
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType() default androidx.annotation.InspectableProperty.ValueType.INFERRED;
+    property @Deprecated public abstract int attributeId;
+    property @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping;
+    property @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping;
+    property @Deprecated public abstract boolean hasAttributeId;
+    property @Deprecated public abstract String name;
+    property @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.EnumEntry {
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int value();
+    property @Deprecated public abstract String name;
+    property @Deprecated public abstract int value;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.FlagEntry {
+    method @Deprecated public abstract int mask() default 0;
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int target();
+    property @Deprecated public abstract int mask;
+    property @Deprecated public abstract String name;
+    property @Deprecated public abstract int target;
+  }
+
+  @Deprecated public enum InspectableProperty.ValueType {
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType COLOR;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType GRAVITY;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INFERRED;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_ENUM;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_FLAG;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType NONE;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType RESOURCE_ID;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface IntDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract int[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract int[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface IntRange {
+    method public abstract long from() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long to() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    property public abstract long from;
+    property public abstract long to;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IntegerRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface InterpolatorRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface Keep {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface LayoutRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface LongDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract long[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract long[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface MainThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface MenuRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface NavigationRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface NonNull {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface NonUiContext {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface Nullable {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface OpenForTesting {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface PluralsRes {
+  }
+
+  @Dimension(unit=androidx.annotation.Dimension.Companion.PX) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface Px {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ReplaceWith {
+    method public abstract String expression();
+    method public abstract String[] imports();
+    property public abstract String expression;
+    property public abstract String[] imports;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
+    method public abstract int api() default 1;
+    method public abstract int value() default 1;
+    property public abstract int api;
+    property public abstract int value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresExtension {
+    method public abstract int extension();
+    method public abstract int version();
+    property public abstract int extension;
+    property public abstract int version;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public static @interface RequiresExtension.Container {
+    method public abstract androidx.annotation.RequiresExtension[] value();
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface RequiresFeature {
+    method public abstract String enforcement();
+    method public abstract String name();
+    property public abstract String enforcement;
+    property public abstract String name;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface RequiresPermission {
+    method public abstract String[] allOf();
+    method public abstract String[] anyOf();
+    method public abstract boolean conditional() default false;
+    method public abstract String value() default "";
+    property public abstract String[] allOf;
+    property public abstract String[] anyOf;
+    property public abstract boolean conditional;
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Read {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Write {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RestrictTo {
+    method public abstract androidx.annotation.RestrictTo.Scope[] value();
+    property public abstract androidx.annotation.RestrictTo.Scope[] value;
+  }
+
+  public enum RestrictTo.Scope {
+    enum_constant @Deprecated public static final androidx.annotation.RestrictTo.Scope GROUP_ID;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ReturnThis {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Size {
+    method public abstract long max() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    method public abstract long min() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long multiple() default 1;
+    method public abstract long value() default -1;
+    property public abstract long max;
+    property public abstract long min;
+    property public abstract long multiple;
+    property public abstract long value;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface StringDef {
+    method public abstract boolean open() default false;
+    method public abstract String[] value();
+    property public abstract boolean open;
+    property public abstract String[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StringRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface TransitionRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface UiContext {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface UiThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface VisibleForTesting {
+    method public abstract int otherwise() default androidx.annotation.VisibleForTesting.PRIVATE;
+    property public abstract int otherwise;
+    field public static final androidx.annotation.VisibleForTesting.Companion Companion;
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  public static final class VisibleForTesting.Companion {
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface WorkerThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface XmlRes {
+  }
+
+}
+
diff --git a/annotation/annotation/api/restricted_1.8.0-beta02.txt b/annotation/annotation/api/restricted_1.8.0-beta02.txt
new file mode 100644
index 0000000..dababc5
--- /dev/null
+++ b/annotation/annotation/api/restricted_1.8.0-beta02.txt
@@ -0,0 +1,372 @@
+// Signature format: 4.0
+package androidx.annotation {
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimatorRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnyRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface AnyThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ArrayRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AttrRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface BinderThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface BoolRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CallSuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CheckResult {
+    method public abstract String suggest() default "";
+    property public abstract String suggest;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ChecksSdkIntAtLeast {
+    method public abstract int api() default -1;
+    method public abstract String codename() default "";
+    method public abstract int extension() default 0;
+    method public abstract int lambda() default -1;
+    method public abstract int parameter() default -1;
+    property public abstract int api;
+    property public abstract String codename;
+    property public abstract int extension;
+    property public abstract int lambda;
+    property public abstract int parameter;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorInt {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorLong {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ColorRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CONSTRUCTOR) public @interface ContentView {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface DeprecatedSinceApi {
+    method public abstract int api();
+    method public abstract String message() default "";
+    property public abstract int api;
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DimenRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Dimension {
+    method public abstract int unit() default androidx.annotation.Dimension.PX;
+    property public abstract int unit;
+    field public static final androidx.annotation.Dimension.Companion Companion;
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  public static final class Dimension.Companion {
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public @interface Discouraged {
+    method public abstract String message();
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface DisplayContext {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface DoNotInline {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DrawableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface EmptySuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface FloatRange {
+    method public abstract double from() default kotlin.jvm.internal.DoubleCompanionObject.NEGATIVE_INFINITY;
+    method public abstract boolean fromInclusive() default true;
+    method public abstract double to() default kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY;
+    method public abstract boolean toInclusive() default true;
+    property public abstract double from;
+    property public abstract boolean fromInclusive;
+    property public abstract double to;
+    property public abstract boolean toInclusive;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FontRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FractionRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface GravityInt {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface GuardedBy {
+    method public abstract String value();
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface HalfFloat {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IdRes {
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InspectableProperty {
+    method @Deprecated public abstract int attributeId() default 0;
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping();
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping();
+    method @Deprecated public abstract boolean hasAttributeId() default true;
+    method @Deprecated public abstract String name() default "";
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType() default androidx.annotation.InspectableProperty.ValueType.INFERRED;
+    property @Deprecated public abstract int attributeId;
+    property @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping;
+    property @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping;
+    property @Deprecated public abstract boolean hasAttributeId;
+    property @Deprecated public abstract String name;
+    property @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.EnumEntry {
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int value();
+    property @Deprecated public abstract String name;
+    property @Deprecated public abstract int value;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.FlagEntry {
+    method @Deprecated public abstract int mask() default 0;
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int target();
+    property @Deprecated public abstract int mask;
+    property @Deprecated public abstract String name;
+    property @Deprecated public abstract int target;
+  }
+
+  @Deprecated public enum InspectableProperty.ValueType {
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType COLOR;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType GRAVITY;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INFERRED;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_ENUM;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_FLAG;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType NONE;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType RESOURCE_ID;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface IntDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract int[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract int[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface IntRange {
+    method public abstract long from() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long to() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    property public abstract long from;
+    property public abstract long to;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IntegerRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface InterpolatorRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface Keep {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface LayoutRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface LongDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract long[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract long[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface MainThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface MenuRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface NavigationRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface NonNull {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface NonUiContext {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface Nullable {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface OpenForTesting {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface PluralsRes {
+  }
+
+  @Dimension(unit=androidx.annotation.Dimension.Companion.PX) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface Px {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ReplaceWith {
+    method public abstract String expression();
+    method public abstract String[] imports();
+    property public abstract String expression;
+    property public abstract String[] imports;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
+    method public abstract int api() default 1;
+    method public abstract int value() default 1;
+    property public abstract int api;
+    property public abstract int value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresExtension {
+    method public abstract int extension();
+    method public abstract int version();
+    property public abstract int extension;
+    property public abstract int version;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public static @interface RequiresExtension.Container {
+    method public abstract androidx.annotation.RequiresExtension[] value();
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface RequiresFeature {
+    method public abstract String enforcement();
+    method public abstract String name();
+    property public abstract String enforcement;
+    property public abstract String name;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface RequiresPermission {
+    method public abstract String[] allOf();
+    method public abstract String[] anyOf();
+    method public abstract boolean conditional() default false;
+    method public abstract String value() default "";
+    property public abstract String[] allOf;
+    property public abstract String[] anyOf;
+    property public abstract boolean conditional;
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Read {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Write {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RestrictTo {
+    method public abstract androidx.annotation.RestrictTo.Scope[] value();
+    property public abstract androidx.annotation.RestrictTo.Scope[] value;
+  }
+
+  public enum RestrictTo.Scope {
+    enum_constant @Deprecated public static final androidx.annotation.RestrictTo.Scope GROUP_ID;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ReturnThis {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Size {
+    method public abstract long max() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    method public abstract long min() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long multiple() default 1;
+    method public abstract long value() default -1;
+    property public abstract long max;
+    property public abstract long min;
+    property public abstract long multiple;
+    property public abstract long value;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface StringDef {
+    method public abstract boolean open() default false;
+    method public abstract String[] value();
+    property public abstract boolean open;
+    property public abstract String[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StringRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface TransitionRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface UiContext {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface UiThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface VisibleForTesting {
+    method public abstract int otherwise() default androidx.annotation.VisibleForTesting.PRIVATE;
+    property public abstract int otherwise;
+    field public static final androidx.annotation.VisibleForTesting.Companion Companion;
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  public static final class VisibleForTesting.Companion {
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface WorkerThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface XmlRes {
+  }
+
+}
+
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricCapture.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricCapture.kt
index 17c9996..d38c3b0 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricCapture.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricCapture.kt
@@ -29,12 +29,21 @@
  */
 @ExperimentalBenchmarkConfigApi
 abstract class MetricCapture(
+    /**
+     * List of names of metrics produced by this MetricCapture.
+     *
+     * The length of this list defines how many metrics will be produced by [captureStart] and
+     * [captureStop].
+     */
     val names: List<String>
 ) {
     /**
      * Starts collecting data for a run.
      *
      * Called at the start of each run.
+     *
+     * @param timeNs Current time, just before starting metrics. Can be used directly to drive a
+     * timing metric produced.
      */
     abstract fun captureStart(timeNs: Long)
 
@@ -54,7 +63,7 @@
      * }
      * ```
      *
-     * @param timeNs Time of metric capture start, in monotonic time (System..
+     * @param timeNs Time of metric capture start, in monotonic time ([java.lang.System.nanoTime])
      * @param output LongArray sized to hold all simultaneous sub metric outputs, use `offset` as
      *  the initial position in `output` to start writing submetrics.
      * @param offset Offset into the output array to start writing sub metrics.
@@ -84,9 +93,11 @@
  * Time metric, which reports time in nanos, based on the time passed to [captureStop].
  *
  * Reports elapsed time with the label from `name`, which defaults to `timeNs`.
+ *
+ * @param name Metric name of the measured time, defaults to `timeNs`.
  */
 @ExperimentalBenchmarkConfigApi
-public class TimeCapture @JvmOverloads constructor(name: String = "timeNs") : MetricCapture(
+class TimeCapture @JvmOverloads constructor(name: String = "timeNs") : MetricCapture(
     names = listOf(name)
 ) {
     private var currentStarted = 0L
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
index fed5b64..54162f0 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
@@ -143,14 +143,30 @@
     fun waitForFileFlush(
         path: String,
         stableIterations: Int,
-        maxIterations: Int,
-        pollDurationMs: Long
+        maxInitialFlushWaitIterations: Int,
+        maxStableFlushWaitIterations: Int,
+        pollDurationMs: Long,
+        triggerFileFlush: () -> Unit
     ) {
+        var lastKnownSize = getFileSizeUnsafe(path)
+
+        triggerFileFlush()
+
+        // first, wait for initial dump from flush, which can be a long amount of time
+        var currentSize = getFileSizeUnsafe(path)
         var iteration = 0
+        while (iteration < maxInitialFlushWaitIterations && currentSize == lastKnownSize) {
+            Thread.sleep(pollDurationMs)
+            currentSize = getFileSizeUnsafe(path)
+            iteration++
+        }
+
+        // wait for stabilization, which should take much less time and happen quickly
+        iteration = 0
+        lastKnownSize = 0
         var stable = 0
-        var lastKnownSize = 0L
-        while (iteration < maxIterations) {
-            val currentSize = getFileSizeUnsafe(path)
+        while (iteration < maxStableFlushWaitIterations) {
+            currentSize = getFileSizeUnsafe(path)
             if (currentSize > 0) {
                 if (currentSize == lastKnownSize) {
                     stable += 1
diff --git a/benchmark/benchmark-macro/api/current.txt b/benchmark/benchmark-macro/api/current.txt
index 000fc7d..7116325 100644
--- a/benchmark/benchmark-macro/api/current.txt
+++ b/benchmark/benchmark-macro/api/current.txt
@@ -202,13 +202,31 @@
     ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label, optional boolean targetPackageOnly);
   }
 
-  public enum TraceSectionMetric.Mode {
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Average;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Count;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode First;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Max;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Min;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Sum;
+  public abstract static sealed class TraceSectionMetric.Mode {
+  }
+
+  public static final class TraceSectionMetric.Mode.Average extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Average INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.Count extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Count INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.First extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.First INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.Max extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Max INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.Min extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Min INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.Sum extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Sum INSTANCE;
   }
 
 }
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index 1f0587c..820522c 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -224,13 +224,31 @@
     ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label, optional boolean targetPackageOnly);
   }
 
-  public enum TraceSectionMetric.Mode {
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Average;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Count;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode First;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Max;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Min;
-    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Sum;
+  public abstract static sealed class TraceSectionMetric.Mode {
+  }
+
+  public static final class TraceSectionMetric.Mode.Average extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Average INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.Count extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Count INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.First extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.First INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.Max extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Max INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.Min extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Min INSTANCE;
+  }
+
+  public static final class TraceSectionMetric.Mode.Sum extends androidx.benchmark.macro.TraceSectionMetric.Mode {
+    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Sum INSTANCE;
   }
 
 }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 5834fe3..a9ba1a8 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -347,21 +347,24 @@
      * @return a [Pair] representing the label, and the absolute path of the method trace.
      */
     internal fun stopMethodTracing(uniqueLabel: String): Pair<String, String> {
-        Shell.executeScriptSilent("am profile stop $packageName")
-        // Wait for the profiles to get dumped :(
-        // ART Method tracing has a buffer size of 8M, so 1 second should be enough
-        // to dump the contents of the buffer.
-
         val tracePath = methodTraceRecordPath(packageName)
-        // Using 50 ms as a poll duration for a max of 20 iterations. This is because
-        // we don't want to wait for longer than 1s. Also, anecdotally when polling from the
-        // shell I found a stable iteration count of 3 to be sufficient.
+
+        // We have to poll here as `am profile stop` is async, but it's hard to calibrate these
+        // numbers, as different devices take drastically different amounts of time.
+        // E.g. pixel 8 takes 100ms for it's full flush, while mokey takes 1700ms to start, then a
+        // few hundred ms to complete.
+        //
+        // Ideally, we'd use the native approach that Studio profilers use (on P+):
+        // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:transport/native/utils/activity_manager.cc;l=111;drc=a4c97db784418341c9f1be60b98ba22301b5ced8
         Shell.waitForFileFlush(
             tracePath,
-            maxIterations = 20,
-            stableIterations = 3,
+            maxInitialFlushWaitIterations = 50, // up to 2.5 sec of waiting on flush to start
+            maxStableFlushWaitIterations = 50, // up to 2.5 sec of waiting on flush to complete
+            stableIterations = 8, // 400ms of stability after flush starts
             pollDurationMs = 50L
-        )
+        ) {
+            Shell.executeScriptSilent("am profile stop $packageName")
+        }
         // unique label so source is clear, dateToFileName so each run of test is unique on host
         val outputFileName = "$uniqueLabel-methodTracing-${dateToFileName()}.trace"
         val stagingFile = File.createTempFile("methodTrace", null, Outputs.dirUsableByAppAndShell)
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index cc26c8f..6437ad16 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -498,14 +498,14 @@
      */
     private val targetPackageOnly: Boolean = true
 ) : Metric() {
-    enum class Mode {
+    sealed class Mode(internal val name: String) {
         /**
          * Captures the duration of the first instance of `sectionName` in the trace.
          *
          * When this mode is used, no measurement will be reported if the named section does
          * not appear in the trace.
          */
-        First,
+        object First : Mode("First")
 
         /**
          * Captures the sum of all instances of `sectionName` in the trace.
@@ -513,7 +513,7 @@
          * When this mode is used, a measurement of `0` will be reported if the named section
          * does not appear in the trace.
          */
-        Sum,
+        object Sum : Mode("Sum")
 
         /**
          * Reports the maximum observed duration for a trace section matching `sectionName` in the
@@ -522,7 +522,7 @@
          * When this mode is used, no measurement will be reported if the named section does
          * not appear in the trace.
          */
-        Min,
+        object Min : Mode("Min")
 
         /**
          * Reports the maximum observed duration for a trace section matching `sectionName` in the
@@ -531,7 +531,7 @@
          * When this mode is used, no measurement will be reported if the named section does
          * not appear in the trace.
          */
-        Max,
+        object Max : Mode("Max")
 
         /**
          * Counts the number of observed instances of a trace section matching `sectionName` in the
@@ -540,7 +540,7 @@
          * When this mode is used, a measurement of `0` will be reported if the named section
          * does not appear in the trace.
          */
-        Count,
+        object Count : Mode("Count")
 
         /**
          * Average duration of trace sections matching `sectionName` in the trace.
@@ -548,7 +548,7 @@
          * When this mode is used, a measurement of `0` will be reported if the named section
          * does not appear in the trace.
          */
-        Average,
+        object Average : Mode("Average")
     }
 
     override fun configure(packageName: String) {
diff --git a/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt b/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
index a691c88..98bc8a7 100644
--- a/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
@@ -79,6 +79,10 @@
     @Test
     fun testXmlAgainstGoldenMicrobenchmark() {
         builder.isMicrobenchmark(true)
+
+        // NOTE: blocklisted arg is removed
+        builder.instrumentationArgsMap["androidx.benchmark.profiling.skipWhenDurationRisksAnr"] =
+            "true"
         MatcherAssert.assertThat(
             builder.buildXml(),
             CoreMatchers.`is`(goldenDefaultConfigBenchmark)
@@ -90,6 +94,10 @@
         builder.isMacrobenchmark(true)
         builder.instrumentationArgsMap["androidx.test.argument1"] = "something1"
         builder.instrumentationArgsMap["androidx.test.argument2"] = "something2"
+
+        // NOTE: blocklisted arg is removed
+        builder.instrumentationArgsMap["androidx.benchmark.profiling.skipWhenDurationRisksAnr"] =
+            "true"
         MatcherAssert.assertThat(
             builder.buildXml(),
             CoreMatchers.`is`(goldenDefaultConfigMacroBenchmark)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index a1a0906..b6f6a93 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -774,6 +774,24 @@
         }
     }
 
+    /**
+     * Enable long-running method tracing on the UI thread, even if that risks ANR for profiling
+     * convenience.
+     *
+     * See [androidx.build.testConfiguration.INST_ARG_BLOCKLIST], which is used to suppress the arg
+     * in CI.
+     */
+    @Suppress("UnstableApiUsage") // usage of HasDeviceTests
+    private fun HasDeviceTests.enableLongMethodTracingInMicrobenchmark(project: Project) {
+        if (project.hasBenchmarkPlugin()) {
+            deviceTests.forEach { deviceTest ->
+                deviceTest.instrumentationRunnerArguments.put(
+                    "androidx.benchmark.profiling.skipWhenDurationRisksAnr", "false"
+                )
+            }
+        }
+    }
+
     private fun Variant.aotCompileMicrobenchmarks(project: Project) {
         if (project.hasBenchmarkPlugin()) {
             @Suppress("UnstableApiUsage") // usage of experimentalProperties
@@ -831,6 +849,7 @@
             onVariants {
                 it.configureTests()
                 it.aotCompileMicrobenchmarks(project)
+                it.enableLongMethodTracingInMicrobenchmark(project)
             }
         }
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
index 01da246..25e9f18 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
@@ -73,9 +73,11 @@
     fun buildJson(): String {
         val gson = GsonBuilder().setPrettyPrinting().create()
         val instrumentationArgsList = mutableListOf<InstrumentationArg>()
-        instrumentationArgsMap.forEach { (key, value) ->
-            instrumentationArgsList.add(InstrumentationArg(key, value))
-        }
+        instrumentationArgsMap
+            .filter { it.key !in INST_ARG_BLOCKLIST }
+            .forEach { (key, value) ->
+                instrumentationArgsList.add(InstrumentationArg(key, value))
+            }
         instrumentationArgsList.addAll(
             if (isMicrobenchmark && !isPostsubmit) {
                 listOf(
@@ -113,12 +115,16 @@
         if (!isPostsubmit && (isMicrobenchmark || isMacrobenchmark)) {
             sb.append(BENCHMARK_PRESUBMIT_INST_ARGS)
         }
-        instrumentationArgsMap.forEach { (key, value) ->
-            sb.append("""
-                <option name="instrumentation-arg" key="$key" value="$value" />
+        instrumentationArgsMap
+            .filter { it.key !in INST_ARG_BLOCKLIST }
+            .forEach { (key, value) ->
+                sb.append(
+                    """
+                    <option name="instrumentation-arg" key="$key" value="$value" />
 
-                """.trimIndent())
-        }
+                    """.trimIndent()
+                )
+            }
         sb.append(SETUP_INCLUDE)
             .append(TARGET_PREPARER_OPEN.replace("CLEANUP_APKS", "true"))
         initialSetupApks.forEach { apk ->
@@ -368,6 +374,13 @@
 """
         .trimIndent()
 
+/**
+ * These args may never be passed in CI, even if they are set per module
+ */
+private val INST_ARG_BLOCKLIST = listOf(
+    "androidx.benchmark.profiling.skipWhenDurationRisksAnr"
+)
+
 private val MICROBENCHMARK_POSTSUBMIT_LISTENERS =
     """
     <option name="device-listeners" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 6eb4d6a..c84401f 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -206,6 +206,9 @@
   @SuppressCompatibility @RequiresApi(21) @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalImageCaptureOutputFormat {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
   }
 
@@ -302,6 +305,7 @@
     method public int getFlashMode();
     method public static androidx.camera.core.ImageCaptureCapabilities getImageCaptureCapabilities(androidx.camera.core.CameraInfo);
     method @IntRange(from=1, to=100) public int getJpegQuality();
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public int getOutputFormat();
     method public androidx.camera.core.resolutionselector.ResolutionSelector? getPostviewResolutionSelector();
     method public androidx.camera.core.ImageCaptureLatencyEstimate getRealtimeCaptureLatencyEstimate();
     method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
@@ -327,6 +331,8 @@
     field public static final int FLASH_MODE_OFF = 2; // 0x2
     field public static final int FLASH_MODE_ON = 1; // 0x1
     field public static final int FLASH_MODE_SCREEN = 3; // 0x3
+    field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG = 0; // 0x0
+    field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1; // 0x1
   }
 
   public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
@@ -336,6 +342,7 @@
     method public androidx.camera.core.ImageCapture.Builder setFlashMode(int);
     method public androidx.camera.core.ImageCapture.Builder setIoExecutor(java.util.concurrent.Executor);
     method public androidx.camera.core.ImageCapture.Builder setJpegQuality(@IntRange(from=1, to=100) int);
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public androidx.camera.core.ImageCapture.Builder setOutputFormat(int);
     method public androidx.camera.core.ImageCapture.Builder setPostviewEnabled(boolean);
     method public androidx.camera.core.ImageCapture.Builder setPostviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
     method public androidx.camera.core.ImageCapture.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
@@ -398,6 +405,7 @@
   }
 
   public interface ImageCaptureCapabilities {
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public java.util.Set<java.lang.Integer!> getSupportedOutputFormats();
     method public boolean isCaptureProcessProgressSupported();
     method public boolean isPostviewSupported();
   }
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 6eb4d6a..c84401f 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -206,6 +206,9 @@
   @SuppressCompatibility @RequiresApi(21) @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalImageCaptureOutputFormat {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
   }
 
@@ -302,6 +305,7 @@
     method public int getFlashMode();
     method public static androidx.camera.core.ImageCaptureCapabilities getImageCaptureCapabilities(androidx.camera.core.CameraInfo);
     method @IntRange(from=1, to=100) public int getJpegQuality();
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public int getOutputFormat();
     method public androidx.camera.core.resolutionselector.ResolutionSelector? getPostviewResolutionSelector();
     method public androidx.camera.core.ImageCaptureLatencyEstimate getRealtimeCaptureLatencyEstimate();
     method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
@@ -327,6 +331,8 @@
     field public static final int FLASH_MODE_OFF = 2; // 0x2
     field public static final int FLASH_MODE_ON = 1; // 0x1
     field public static final int FLASH_MODE_SCREEN = 3; // 0x3
+    field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG = 0; // 0x0
+    field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1; // 0x1
   }
 
   public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
@@ -336,6 +342,7 @@
     method public androidx.camera.core.ImageCapture.Builder setFlashMode(int);
     method public androidx.camera.core.ImageCapture.Builder setIoExecutor(java.util.concurrent.Executor);
     method public androidx.camera.core.ImageCapture.Builder setJpegQuality(@IntRange(from=1, to=100) int);
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public androidx.camera.core.ImageCapture.Builder setOutputFormat(int);
     method public androidx.camera.core.ImageCapture.Builder setPostviewEnabled(boolean);
     method public androidx.camera.core.ImageCapture.Builder setPostviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
     method public androidx.camera.core.ImageCapture.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
@@ -398,6 +405,7 @@
   }
 
   public interface ImageCaptureCapabilities {
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public java.util.Set<java.lang.Integer!> getSupportedOutputFormats();
     method public boolean isCaptureProcessProgressSupported();
     method public boolean isPostviewSupported();
   }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalImageCaptureOutputFormat.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalImageCaptureOutputFormat.java
new file mode 100644
index 0000000..359f0c3
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalImageCaptureOutputFormat.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.RequiresOptIn;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Denotes that the annotated method uses an experimental path for configuring output format
+ * of {@link ImageCapture} or related querying in {@link ImageCaptureCapabilities}.
+ */
+@Retention(CLASS)
+@RequiresOptIn
+public @interface ExperimentalImageCaptureOutputFormat {
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 463a84d..74a3b40 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -136,8 +136,10 @@
 
 import java.io.File;
 import java.io.OutputStream;
+import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -292,17 +294,23 @@
     public static final int FLASH_TYPE_USE_TORCH_AS_FLASH = 1;
 
     /**
-     * Captures SDR image using the {@link ImageFormat#JPEG} image format.
+     * Captures 8-bit standard dynamic range (SDR) images using the {@link ImageFormat#JPEG}
+     * image format.
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
+    @ExperimentalImageCaptureOutputFormat
     public static final int OUTPUT_FORMAT_JPEG = 0;
 
     /**
      * Captures Ultra HDR compressed images using the {@link ImageFormat#JPEG_R} image format.
-     * This format is backward compatible with SDR JPEG images and supports HDR rendering of
-     * content.
+     *
+     * <p>This format is backward compatible with SDR JPEG images and supports HDR rendering of
+     * content. This means that on older apps or devices, images appear seamlessly as regular JPEG;
+     * on apps and devices that have been updated to fully support the format, images appear as HDR.
+     *
+     * <p>For more information see
+     * <a href="https://developer.android.com/media/grow/ultra-hdr">Support Ultra HDR</a>.
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
+    @ExperimentalImageCaptureOutputFormat
     public static final int OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1;
 
     /**
@@ -503,6 +511,7 @@
         return false;
     }
 
+    @OptIn(markerClass = ExperimentalImageCaptureOutputFormat.class)
     private static boolean isOutputFormatUltraHdr(@NonNull MutableConfig config) {
         return Objects.equals(config.retrieveOption(OPTION_OUTPUT_FORMAT, null),
                 OUTPUT_FORMAT_JPEG_ULTRA_HDR);
@@ -849,10 +858,9 @@
      *
      * @see ImageCapture.Builder#setOutputFormat(int)
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
+    @ExperimentalImageCaptureOutputFormat
     @OutputFormat
-    @NonNull
-    public Integer getOutputFormat() {
+    public int getOutputFormat() {
         return checkNotNull(getCurrentConfig().retrieveOption(OPTION_OUTPUT_FORMAT,
                 Defaults.DEFAULT_OUTPUT_FORMAT));
     }
@@ -963,10 +971,10 @@
             return false;
         }
 
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @ExperimentalImageCaptureOutputFormat
         @NonNull
         @Override
-        public Set<Integer> getSupportedOutputFormats() {
+        public Set<@OutputFormat Integer> getSupportedOutputFormats() {
             Set<Integer> formats = new HashSet<>();
             formats.add(OUTPUT_FORMAT_JPEG);
             if (isUltraHdrSupported()) {
@@ -1574,6 +1582,8 @@
     /**
      * The output format of the captured image.
      */
+    @OptIn(markerClass = androidx.camera.core.ExperimentalImageCaptureOutputFormat.class)
+    @Target({ElementType.TYPE_USE})
     @IntDef({OUTPUT_FORMAT_JPEG, OUTPUT_FORMAT_JPEG_ULTRA_HDR})
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -1836,6 +1846,7 @@
      * <p>These values may be overridden by the implementation. They only provide a minimum set of
      * defaults that are implementation independent.
      */
+    @OptIn(markerClass = androidx.camera.core.ExperimentalImageCaptureOutputFormat.class)
     @RestrictTo(Scope.LIBRARY_GROUP)
     public static final class Defaults
             implements ConfigProvider<ImageCaptureConfig> {
@@ -2805,7 +2816,12 @@
          * <p>If not set, the output format will default to {@link #OUTPUT_FORMAT_JPEG}.
          *
          * <p>If an Ultra HDR output format is used, a {@link DynamicRange#HDR_UNSPECIFIED_10_BIT}
-         * will be used as the dynamic range of this use case.
+         * will be used as the dynamic range of this use case. Please note that some devices may not
+         * be able to support configuring both SDR and HDR use cases at the same time, e.g. use
+         * Ultra HDR ImageCapture with a SDR Preview. Configuring concurrent SDR and HDR on these
+         * devices will result in an {@link IllegalArgumentException} to be thrown when invoking
+         * {@code bindToLifecycle()}. Such device specific constraints can be queried by calling
+         * {@link android.hardware.camera2.params.DynamicRangeProfiles#getProfileCaptureRequestConstraints(long)}.
          *
          * @param outputFormat The output image format. Value is {@link #OUTPUT_FORMAT_JPEG} or
          *                     {@link #OUTPUT_FORMAT_JPEG_ULTRA_HDR}.
@@ -2814,7 +2830,7 @@
          * @see OutputFormat
          * @see ImageCaptureCapabilities#getSupportedOutputFormats()
          */
-        @RestrictTo(Scope.LIBRARY_GROUP)
+        @ExperimentalImageCaptureOutputFormat
         @NonNull
         public Builder setOutputFormat(@OutputFormat int outputFormat) {
             getMutableConfig().insertOption(OPTION_OUTPUT_FORMAT, outputFormat);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureCapabilities.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureCapabilities.java
index 0469165..93c21ea 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureCapabilities.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureCapabilities.java
@@ -17,7 +17,6 @@
 package androidx.camera.core;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
 
 import java.util.Set;
 
@@ -50,11 +49,14 @@
     /**
      * Gets the supported {@link ImageCapture.OutputFormat} set.
      *
+     * <p>The set returned will always contain {@link ImageCapture.OutputFormat#OUTPUT_FORMAT_JPEG}
+     * format, support for other formats will vary by camera.
+     *
      * @return a set of supported output formats.
      *
      * @see ImageCapture.Builder#setOutputFormat(int)
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @ExperimentalImageCaptureOutputFormat
     @NonNull
-    Set<Integer> getSupportedOutputFormats();
+    Set<@ImageCapture.OutputFormat Integer> getSupportedOutputFormats();
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index deee1e6..eadb809 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -48,6 +48,7 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.Camera;
@@ -953,6 +954,7 @@
         return is10Bit || isHdr;
     }
 
+    @OptIn(markerClass = androidx.camera.core.ExperimentalImageCaptureOutputFormat.class)
     private static boolean hasUltraHdrImageCapture(@NonNull Collection<UseCase> useCases) {
         for (UseCase useCase : useCases) {
             if (!isImageCapture(useCase)) {
diff --git a/camera/camera-lifecycle/api/current.txt b/camera/camera-lifecycle/api/current.txt
index 8300968..4c7dede 100644
--- a/camera/camera-lifecycle/api/current.txt
+++ b/camera/camera-lifecycle/api/current.txt
@@ -28,5 +28,9 @@
     method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.lifecycle.ProcessCameraProvider> getInstance(android.content.Context context);
   }
 
+  public final class ProcessCameraProviderExtKt {
+    method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public static suspend Object? awaitInstance(androidx.camera.lifecycle.ProcessCameraProvider.Companion, android.content.Context context, kotlin.coroutines.Continuation<? super androidx.camera.lifecycle.ProcessCameraProvider>);
+  }
+
 }
 
diff --git a/camera/camera-lifecycle/api/restricted_current.txt b/camera/camera-lifecycle/api/restricted_current.txt
index 8300968..4c7dede 100644
--- a/camera/camera-lifecycle/api/restricted_current.txt
+++ b/camera/camera-lifecycle/api/restricted_current.txt
@@ -28,5 +28,9 @@
     method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.lifecycle.ProcessCameraProvider> getInstance(android.content.Context context);
   }
 
+  public final class ProcessCameraProviderExtKt {
+    method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public static suspend Object? awaitInstance(androidx.camera.lifecycle.ProcessCameraProvider.Companion, android.content.Context context, kotlin.coroutines.Continuation<? super androidx.camera.lifecycle.ProcessCameraProvider>);
+  }
+
 }
 
diff --git a/camera/camera-lifecycle/build.gradle b/camera/camera-lifecycle/build.gradle
index b5b49c3..ee6f2be 100644
--- a/camera/camera-lifecycle/build.gradle
+++ b/camera/camera-lifecycle/build.gradle
@@ -35,7 +35,9 @@
     api(project(":camera:camera-core"))
     implementation("androidx.core:core:1.1.0")
     implementation(libs.autoValueAnnotations)
-    implementation("androidx.concurrent:concurrent-futures:1.0.0")
+    implementation(libs.kotlinCoroutinesAndroid)
+    implementation("androidx.concurrent:concurrent-futures:1.1.0")
+    implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
 
     annotationProcessor(libs.autoValue)
 
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProviderExt.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProviderExt.kt
new file mode 100644
index 0000000..15c9991
--- /dev/null
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProviderExt.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.lifecycle
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.InitializationException
+import androidx.concurrent.futures.await
+
+/**
+ * Retrieves the [ProcessCameraProvider].
+ *
+ * This is a suspending function unlike [ProcessCameraProvider.getInstance] which returns a
+ * [com.google.common.util.concurrent.ListenableFuture].
+ *
+ * @param context The application context.
+ * @return A fully initialized ProcessCameraProvider for the current process.
+ * @throws InitializationException If failed to retrieve the ProcessCameraProvider, use
+ * [InitializationException.cause] to get the error cause.
+ * @see ProcessCameraProvider.getInstance
+ */
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+suspend fun ProcessCameraProvider.Companion.awaitInstance(context: Context) =
+    getInstance(context).await()
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 6eb0688..34e5bb3 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
@@ -131,6 +131,7 @@
 import androidx.camera.video.internal.compat.quirk.PreviewDelayWhenVideoCaptureIsBoundQuirk;
 import androidx.camera.video.internal.compat.quirk.PreviewStretchWhenVideoCaptureIsBoundQuirk;
 import androidx.camera.video.internal.compat.quirk.SizeCannotEncodeVideoQuirk;
+import androidx.camera.video.internal.compat.quirk.TemporalNoiseQuirk;
 import androidx.camera.video.internal.compat.quirk.VideoQualityQuirk;
 import androidx.camera.video.internal.config.VideoMimeInfo;
 import androidx.camera.video.internal.encoder.SwappedVideoEncoderInfo;
@@ -195,8 +196,10 @@
                 hasVideoQualityQuirkAndWorkaroundBySurfaceProcessing();
         boolean hasExtraSupportedResolutionQuirk =
                 DeviceQuirks.get(ExtraSupportedResolutionQuirk.class) != null;
+        boolean hasTemporalNoiseQuirk = DeviceQuirks.get(TemporalNoiseQuirk.class) != null;
         USE_TEMPLATE_PREVIEW_BY_QUIRK =
-                hasPreviewStretchQuirk || hasPreviewDelayQuirk || hasImageCaptureFailedQuirk;
+                hasPreviewStretchQuirk || hasPreviewDelayQuirk || hasImageCaptureFailedQuirk
+                        || hasTemporalNoiseQuirk;
         sEnableSurfaceProcessingByQuirk =
                 hasPreviewDelayQuirk || hasImageCaptureFailedQuirk
                         || hasVideoQualityQuirkAndWorkaroundBySurfaceProcessing
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoRecordEvent.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoRecordEvent.java
index 7549f7f..675343f 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoRecordEvent.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoRecordEvent.java
@@ -410,7 +410,11 @@
         /**
          * Gets the error cause.
          *
-         * <p>Returns {@code null} if {@link #hasError()} returns {@code false}.
+         * <p>Returns the error cause if any, otherwise returns {@code null}.
+         * <p>Note that not all error types include an error cause. For some error types, the
+         * file may still be generated successfully with no error cause. For example,
+         * {@link #ERROR_FILE_SIZE_LIMIT_REACHED}, {@link #ERROR_DURATION_LIMIT_REACHED} and
+         * {@link #ERROR_SOURCE_INACTIVE}.
          */
         @Nullable
         public Throwable getCause() {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
index d9e9964..93c5c27 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -110,6 +110,9 @@
         if (SizeCannotEncodeVideoQuirk.load()) {
             quirks.add(new SizeCannotEncodeVideoQuirk());
         }
+        if (TemporalNoiseQuirk.load()) {
+            quirks.add(new TemporalNoiseQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/TemporalNoiseQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/TemporalNoiseQuirk.java
new file mode 100644
index 0000000..72cfa06
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/TemporalNoiseQuirk.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.compat.quirk;
+
+import android.hardware.camera2.CameraDevice;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: b/316560705
+ *     Description: Quirk indicates the recorded video contains obvious temporal noise. The issue
+ *                  happens on Pixel 8 front camera and when the template type is
+ *                  {@link CameraDevice#TEMPLATE_RECORD}.
+ *     Device(s): Pixel 8.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class TemporalNoiseQuirk implements Quirk {
+
+    static boolean load() {
+        return isPixel8();
+    }
+
+    private static boolean isPixel8() {
+        return "Pixel 8".equalsIgnoreCase(Build.MODEL);
+    }
+}
diff --git a/camera/integration-tests/coretestapp/lint-baseline.xml b/camera/integration-tests/coretestapp/lint-baseline.xml
index 8b17bcf..2fe575f 100644
--- a/camera/integration-tests/coretestapp/lint-baseline.xml
+++ b/camera/integration-tests/coretestapp/lint-baseline.xml
@@ -12,15 +12,6 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="    private @ImageCapture.OutputFormat int mImageOutputFormat = OUTPUT_FORMAT_JPEG;"
-        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
         message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                    if (canDeviceWriteToMediaStore()) {"
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -84,15 +75,6 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="ImageCaptureCapabilities.getSupportedOutputFormats can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="            Set&lt;Integer> supportedOutputFormats = capabilities.getSupportedOutputFormats();"
-        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
         message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                        videoFilePath = getAbsolutePathFromUri("
         errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~">
@@ -174,15 +156,6 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        } else if (mImageOutputFormat == OUTPUT_FORMAT_JPEG_ULTRA_HDR"
-        errorLine2="                                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
         message="Camera.isUseCasesCombinationSupported can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        return mCamera.isUseCasesCombinationSupported(buildUseCases().toArray(new UseCase[0]));"
         errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -264,78 +237,6 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (format == OUTPUT_FORMAT_JPEG) {"
-        errorLine2="                      ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        } else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) {"
-        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (format == OUTPUT_FORMAT_JPEG) {"
-        errorLine2="                      ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        } else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) {"
-        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (format == OUTPUT_FORMAT_JPEG) {"
-        errorLine2="                      ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        } else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) {"
-        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                return OUTPUT_FORMAT_JPEG;"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                return OUTPUT_FORMAT_JPEG_ULTRA_HDR;"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
         message="FileUtil.createFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        if (!createFolder(pictureFolder)) {"
         errorLine2="             ~~~~~~~~~~~~">
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CaptureOptionSubmissionTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CaptureOptionSubmissionTest.kt
new file mode 100644
index 0000000..5c53be8
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CaptureOptionSubmissionTest.kt
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core
+
+import android.content.Context
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.TotalCaptureResult
+import android.util.Range
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.camera2.pipe.integration.adapter.awaitUntil
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.integration.core.util.CameraPipeUtil
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.SurfaceTextureProvider
+import androidx.camera.testing.impl.WakelockEmptyActivityRule
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.video.Recorder
+import androidx.camera.video.VideoCapture
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Assume
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Tests for checking if a capture option is submitted from end-to-end.
+ *
+ * Usually, these tests only check if the corresponding capture request is submitted properly by
+ * checking the [CaptureRequest] with `Camera2Interop` capture callback. This does not mean the
+ * framework will always honor these capture request options, so we don't usually care about the
+ * result. If the [TotalCaptureResult] or response by camera also need to be verified for some
+ * specific cases, we may need to have additional considerations and will probably vary in a
+ * case-to-case basis.
+ *
+ * TODO: Will probably be better to use CameraController whenever possible to increase the scope.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class CaptureOptionSubmissionTest(
+    private val selectorName: String,
+    private val cameraSelector: CameraSelector,
+    private val implName: String,
+    private val cameraConfig: CameraXConfig
+) {
+    @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
+    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(cameraConfig)
+    )
+
+    @get:Rule
+    val wakelockEmptyActivityRule = WakelockEmptyActivityRule()
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var cameraProvider: ProcessCameraProvider
+    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+
+    // Capture callback added to session, so only a repeating capture callback, not non-repeating
+    private lateinit var sessionCaptureCallback: CaptureCallback
+
+    @Before
+    fun setUp(): Unit = runBlocking {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+
+        ProcessCameraProvider.configureInstance(cameraConfig)
+        cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+        sessionCaptureCallback = CaptureCallback()
+
+        withContext(Dispatchers.Main) {
+            fakeLifecycleOwner = FakeLifecycleOwner()
+            fakeLifecycleOwner.startAndResume()
+        }
+    }
+
+    @After
+    fun tearDown(): Unit = runBlocking {
+        if (::cameraProvider.isInitialized) {
+            withContext(Dispatchers.Main) {
+                cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
+            }
+        }
+    }
+
+    /*
+     * Only testing if a supported FPS range is submitted to camera in [CaptureRequest] without
+     * caring about the result. This test basically checks if [Preview.Builder.setTargetFrameRate]
+     * works properly.
+     */
+
+    @Test
+    fun canSubmitSupportedAeTargetFpsRanges_whenTargetFrameRateSetToPreviewOnly() = runBlocking {
+        assumeTrue(
+            "TODO(b/331900702): Enable when the bug is fixed at camera-pipe",
+            implName != CameraPipeConfig::class.simpleName
+        )
+
+        assumeTrue(
+            "TODO(b/332235883): Enable for legacy when the bug is resolved",
+            !isHwLevelLegacy()
+        )
+
+        // At least 2 FPS ranges should be checked as the submitted range may just be from template
+        getSupportedFpsRanges().forEach { targetFpsRange ->
+            if (targetFpsRange.upper > 30) {
+                // TODO: b/332464740 - High FPS may not be supported as per stream config map
+                return@forEach
+            }
+
+            var lastSubmittedFpsRange: Range<Int>? = null
+            val result = sessionCaptureCallback.verify { captureRequest, _ ->
+                captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE]?.let {
+                    lastSubmittedFpsRange = it
+                }
+                captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] == targetFpsRange
+            }
+
+            bindUseCases(listOf(Preview.Builder().setTargetFrameRate(targetFpsRange)))
+
+            assertWithMessage(
+                "Test failed for targetFpsRange = $targetFpsRange" +
+                    ", lastSubmittedFpsRange = $lastSubmittedFpsRange"
+            ).that(result.awaitUntil(timeoutMillis = 10000)).isTrue()
+
+            unbindAllUseCases()
+        }
+    }
+
+    @Test
+    fun canSubmitSupportedAeTargetFpsRanges_whenTargetFrameRateSetToVideoCaptureOnly() =
+        runBlocking {
+            assumeTrue(
+                "TODO(b/331900702): Enable when the bug is fixed at camera-pipe",
+                implName != CameraPipeConfig::class.simpleName
+            )
+
+            assumeTrue(
+                "TODO(b/332235883): Enable for legacy when the bug is resolved",
+                !isHwLevelLegacy()
+            )
+
+            // At least 2 FPS ranges should be checked as the submitted range may be from template
+            getSupportedFpsRanges().forEach { targetFpsRange ->
+                if (targetFpsRange.upper > 30) {
+                    // TODO: b/332464740 - High FPS may not be supported as per stream config map
+                    return@forEach
+                }
+
+                var lastSubmittedFpsRange: Range<Int>? = null
+                val result = sessionCaptureCallback.verify { captureRequest, _ ->
+                    captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE]?.let {
+                        lastSubmittedFpsRange = it
+                    }
+                    captureRequest[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] == targetFpsRange
+                }
+
+                bindUseCases(
+                    listOf(
+                        VideoCapture.Builder(Recorder.Builder().build())
+                            .setTargetFrameRate(targetFpsRange),
+                        // TODO: b/333365764 - Remove extra Preview use case added to avoid capture
+                        //  session failure due to MediaCodec error
+                        Preview.Builder()
+                    )
+                )
+
+                assertWithMessage(
+                    "Test failed for targetFpsRange = $targetFpsRange" +
+                        ", lastSubmittedFpsRange = $lastSubmittedFpsRange"
+                ).that(result.awaitUntil(timeoutMillis = 10000)).isTrue()
+
+                unbindAllUseCases()
+            }
+        }
+
+    // TODO: b/332464991 - Add a FPS test adding different FPS ranges to Preview & VideoCapture
+
+    // TODO - Adds tests to check capture option is consistent for both non-repeating and repeating
+    //  captures. E.g., FPS range is not submitted for non-repeating capture right now. But this
+    //  will probably require us to add Camera2Interop callback for non-repeating captures as well,
+    //  something that comes up every now and then, although low priority.
+
+    private fun getSupportedFpsRanges(): Array<Range<Int>> {
+        val cameraCharacteristics = CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
+        Assume.assumeNotNull(cameraCharacteristics)
+
+        val fpsRanges = cameraCharacteristics!!.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
+        Assume.assumeNotNull(fpsRanges)
+
+        return fpsRanges!!
+    }
+
+    private fun isHwLevelLegacy(): Boolean {
+        val cameraCharacteristics = CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
+        Assume.assumeNotNull(cameraCharacteristics)
+
+        val hwLevel = cameraCharacteristics!!.get(INFO_SUPPORTED_HARDWARE_LEVEL)
+        Assume.assumeNotNull(hwLevel)
+
+        return hwLevel == INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+    }
+
+    private suspend fun bindUseCases(
+        useCaseBuilders: List<UseCaseConfig.Builder<*, *, *>> = listOf(Preview.Builder())
+    ) {
+        if (useCaseBuilders.isEmpty()) {
+            return
+        }
+
+        withContext(Dispatchers.Main) {
+            val useCases = mutableListOf<UseCase>()
+
+            useCaseBuilders.forEachIndexed { index, builder ->
+                useCases.add(builder.also {
+                    if (index == 0) { // adding to just one use case is enough
+                        CameraPipeUtil.setCameraCaptureSessionCallback(
+                            implName,
+                            it,
+                            sessionCaptureCallback
+                        )
+                    }
+                }.build().apply {
+                    if (this is Preview) {
+                        setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+                    }
+                })
+            }
+
+            cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                cameraSelector,
+                *useCases.toTypedArray()
+            )
+        }
+    }
+
+    private suspend fun unbindAllUseCases() {
+        withContext(Dispatchers.Main) {
+            cameraProvider.unbindAll()
+        }
+    }
+
+    class CaptureCallback : CameraCaptureSession.CaptureCallback() {
+        data class Verification(
+            val condition: (
+                captureRequest: CaptureRequest,
+                captureResult: TotalCaptureResult
+            ) -> Boolean,
+            val isVerified: CompletableDeferred<Unit>
+        )
+
+        private var pendingVerifications = mutableListOf<Verification>()
+
+        /**
+         * Returns a [Deferred] representing if verification has been completed
+         */
+        fun verify(
+            condition: (
+                captureRequest: CaptureRequest,
+                captureResult: TotalCaptureResult
+            ) -> Boolean = { _, _ -> false },
+        ): Deferred<Unit> = CompletableDeferred<Unit>().apply {
+            val verification = Verification(condition, this)
+            pendingVerifications.add(verification)
+
+            invokeOnCompletion {
+                pendingVerifications.remove(verification)
+            }
+        }
+
+        override fun onCaptureCompleted(
+            session: CameraCaptureSession,
+            request: CaptureRequest,
+            result: TotalCaptureResult
+        ) {
+            pendingVerifications.forEach {
+                if (it.condition(request, result)) {
+                    it.isVerified.complete(Unit)
+                }
+            }
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "selector={0},config={2}")
+        fun data() = listOf(
+            arrayOf(
+                "back",
+                CameraSelector.DEFAULT_BACK_CAMERA,
+                Camera2Config::class.simpleName,
+                Camera2Config.defaultConfig()
+            ),
+            arrayOf(
+                "back",
+                CameraSelector.DEFAULT_BACK_CAMERA,
+                CameraPipeConfig::class.simpleName,
+                CameraPipeConfig.defaultConfig()
+            ),
+            // front camera is not important with the current test, but may be required in future
+        )
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 710e7ab..c8936d3 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -207,7 +207,7 @@
     @Suppress("DEPRECATION") // test for legacy resolution API
     private fun takeImageAndVerifySize(
         cameraSelector: CameraSelector = BACK_SELECTOR,
-        @ImageCapture.OutputFormat outputFormat: Int = OUTPUT_FORMAT_JPEG,
+        outputFormat: @ImageCapture.OutputFormat Int = OUTPUT_FORMAT_JPEG,
     ): Unit = runBlocking {
         // Arrange.
         val useCaseBuilder = ImageCapture.Builder()
@@ -499,7 +499,7 @@
 
     private fun saveToUri(
         cameraSelector: CameraSelector = BACK_SELECTOR,
-        @ImageCapture.OutputFormat outputFormat: Int = OUTPUT_FORMAT_JPEG,
+        outputFormat: @ImageCapture.OutputFormat Int = OUTPUT_FORMAT_JPEG,
     ): Unit = runBlocking {
         // Arrange.
         val useCaseBuilder = defaultBuilder
@@ -552,7 +552,7 @@
 
     private fun saveToOutputStream(
         cameraSelector: CameraSelector = BACK_SELECTOR,
-        @ImageCapture.OutputFormat outputFormat: Int = OUTPUT_FORMAT_JPEG,
+        outputFormat: @ImageCapture.OutputFormat Int = OUTPUT_FORMAT_JPEG,
     ) = runBlocking {
         // Arrange.
         val useCaseBuilder = defaultBuilder
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 36d749d..98f69cb 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
@@ -108,6 +108,7 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.DisplayOrientedMeteringPointFactory;
 import androidx.camera.core.DynamicRange;
+import androidx.camera.core.ExperimentalImageCaptureOutputFormat;
 import androidx.camera.core.ExperimentalLensFacing;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
@@ -185,6 +186,8 @@
  * resumed and when the device is rotated. The complex interactions between the camera and these
  * lifecycle events are handled internally by CameraX.
  */
+@SuppressLint("NullAnnotationGroup")
+@OptIn(markerClass = ExperimentalImageCaptureOutputFormat.class)
 public class CameraXActivity extends AppCompatActivity {
     private static final String TAG = "CameraXActivity";
     private static final String[] REQUIRED_PERMISSIONS;
diff --git a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/EasingTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/EasingTest.kt
index f76a867..962c6dc 100644
--- a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/EasingTest.kt
+++ b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/EasingTest.kt
@@ -27,6 +27,7 @@
 import androidx.core.graphics.component3
 import androidx.core.graphics.component4
 import androidx.core.graphics.createBitmap
+import androidx.core.graphics.withTranslation
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import kotlin.math.abs
@@ -36,6 +37,7 @@
 import org.junit.runner.RunWith
 
 const val BitmapSize = 128
+const val Margin = 16
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -57,10 +59,11 @@
         // - Compare the two bitmaps to validate that our cubic Bézier evaluation matches that
         //   of Skia/Canvas
         for (cubic in listOf(
-            floatArrayOf(0.4f, 0.0f, 0.2f, 1.0f), // FastOutSlowInEasing
-            floatArrayOf(0.0f, 0.0f, 0.2f, 1.0f), // LinearOutSlowInEasing
-            floatArrayOf(0.4f, 0.0f, 1.0f, 1.0f), // FastOutLinearInEasing
-            floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f),
+            floatArrayOf(0.40f, 0.00f, 0.20f, 1.0f), // FastOutSlowInEasing
+            floatArrayOf(0.00f, 0.00f, 0.20f, 1.0f), // LinearOutSlowInEasing
+            floatArrayOf(0.40f, 0.00f, 1.00f, 1.0f), // FastOutLinearInEasing
+            floatArrayOf(0.34f, 1.56f, 0.64f, 1.0f), // EaseOutBack (overshoot)
+            floatArrayOf(0.00f, 1.00f, 0.00f, 1.0f),
         )) {
             val path = Path().apply {
                 cubicTo(
@@ -73,22 +76,30 @@
                 )
             }
 
-            val reference = createBitmap(BitmapSize, BitmapSize).applyCanvas {
-                drawPath(path.asAndroidPath(), paint)
+            val reference = createBitmap(
+                BitmapSize + 2 * Margin,
+                BitmapSize + 2 * Margin
+            ).applyCanvas {
+                withTranslation(Margin.toFloat(), Margin.toFloat()) {
+                    drawPath(path.asAndroidPath(), paint)
+                }
             }
 
             val easing = CubicBezierEasing(cubic[0], cubic[1], cubic[2], cubic[3])
-            val subject = createBitmap(BitmapSize, BitmapSize).applyCanvas {
+            val subject = createBitmap(
+                BitmapSize + 2 * Margin,
+                BitmapSize + 2 * Margin
+            ).applyCanvas {
                 var x0 = 0.0f
                 var y0 = 0.0f
                 for (x in 1 until BitmapSize) {
                     val x1 = x / BitmapSize.toFloat()
                     val y1 = easing.transform(x1)
                     drawLine(
-                        x0 * BitmapSize,
-                        y0 * BitmapSize,
-                        x1 * BitmapSize,
-                        y1 * BitmapSize,
+                        Margin.toFloat() + x0 * BitmapSize,
+                        Margin.toFloat() + y0 * BitmapSize,
+                        Margin.toFloat() + x1 * BitmapSize,
+                        Margin.toFloat() + y1 * BitmapSize,
                         paint
                     )
                     x0 = x1
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
index 1f86dd7..eb5c46e 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
@@ -20,6 +20,7 @@
 import com.google.common.truth.Truth.assertThat
 import junit.framework.TestCase.assertEquals
 import junit.framework.TestCase.assertTrue
+import kotlin.math.ulp
 import org.junit.Assert.assertNotEquals
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -27,6 +28,9 @@
 
 @RunWith(JUnit4::class)
 class EasingTest {
+    val ZeroEpsilon = -(1.0f.ulp)
+    val OneEpsilon = 1.0f + 1.0f.ulp
+
     @Test
     fun cubicBezierStartsAt0() {
         val easing = FastOutSlowInEasing
@@ -96,7 +100,7 @@
         // Test the last 16 ulps until 1.0f
         for (i in 0x3f7ffff0..0x3f7fffff) {
             val t = curve.transform(floatFromBits(i))
-            assertTrue(t in 0.0f..1.0f)
+            assertTrue(t in -ZeroEpsilon..OneEpsilon)
         }
     }
 }
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Easing.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Easing.kt
index 0b6e7d6..51dbbbf 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Easing.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Easing.kt
@@ -20,7 +20,6 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.graphics.evaluateCubic
 import androidx.compose.ui.graphics.findFirstCubicRoot
-import androidx.compose.ui.util.fastCoerceIn
 
 /**
  * Easing is a way to adjust an animation’s fraction. Easing allows transitioning
@@ -137,8 +136,10 @@
                 )
             }
 
-            // Clamp to clean up numerical imprecision at the extremes
-            evaluateCubic(b, d, t).fastCoerceIn(0f, 1f)
+            // Don't clamp the values since the curve might be used to over- or under-shoot
+            // The test above that checks if fraction is in ]0..1[ will ensure we start and
+            // end at 0 and 1 respectively
+            evaluateCubic(b, d, t)
         } else {
             fraction
         }
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
index 1e6d08b..9d40cbe 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
@@ -49,7 +49,6 @@
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Density
@@ -70,7 +69,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
-@OptIn(ExperimentalTestApi::class, InternalAnimationApi::class)
+@OptIn(InternalAnimationApi::class)
 class AnimatedVisibilityTest {
 
     @get:Rule
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
index 32c7532..9d8173b 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
@@ -46,7 +46,6 @@
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
@@ -74,7 +73,6 @@
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
-@OptIn(ExperimentalTestApi::class)
 class AnimationModifierTest {
 
     @get:Rule
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/CrossfadeTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
index df47702..21e8dfe 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
@@ -31,7 +31,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.withFrameMillis
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.unit.dp
@@ -46,7 +45,6 @@
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
-@OptIn(ExperimentalTestApi::class)
 class CrossfadeTest {
 
     @get:Rule
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 62bdfbf..97c0dec 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -135,6 +135,7 @@
             12300 to "1.7.0-alpha04",
             12400 to "1.7.0-alpha05",
             12500 to "1.7.0-alpha06",
+            12600 to "1.7.0-alpha07",
         )
 
         /**
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 7a417d7..751b7a5 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,4 +1,6 @@
 // Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.foundation.lazy.grid.LazyGridItemScope#animateItem(androidx.compose.ui.Modifier, androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>, androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>, androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>):
+    Added method androidx.compose.foundation.lazy.grid.LazyGridItemScope.animateItem(androidx.compose.ui.Modifier,androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>,androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>,androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>)
 AddedAbstractMethod: androidx.compose.foundation.pager.PageInfo#getKey():
     Added method androidx.compose.foundation.pager.PageInfo.getKey()
 AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getBeyondViewportPageCount():
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index b6f1ca2..f2fda0e 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -962,7 +962,8 @@
   }
 
   @androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
+    method public androidx.compose.ui.Modifier animateItem(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? fadeInSpec, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>? placementSpec, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? fadeOutSpec);
+    method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public default androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
   }
 
   @androidx.compose.foundation.lazy.grid.LazyGridScopeMarker public sealed interface LazyGridItemSpanScope {
@@ -1413,10 +1414,6 @@
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.geometry.Rect calculateRectForParent(androidx.compose.ui.geometry.Rect localRect);
   }
 
-  public final class ScrollIntoView {
-    method public static suspend Object? scrollIntoView(androidx.compose.ui.node.DelegatableNode, optional androidx.compose.ui.geometry.Rect? rect, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-  }
-
 }
 
 package androidx.compose.foundation.selection {
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 7a417d7..751b7a5 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,4 +1,6 @@
 // Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.foundation.lazy.grid.LazyGridItemScope#animateItem(androidx.compose.ui.Modifier, androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>, androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>, androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>):
+    Added method androidx.compose.foundation.lazy.grid.LazyGridItemScope.animateItem(androidx.compose.ui.Modifier,androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>,androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>,androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>)
 AddedAbstractMethod: androidx.compose.foundation.pager.PageInfo#getKey():
     Added method androidx.compose.foundation.pager.PageInfo.getKey()
 AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getBeyondViewportPageCount():
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 0d2cf26..717e980 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -964,7 +964,8 @@
   }
 
   @androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
+    method public androidx.compose.ui.Modifier animateItem(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? fadeInSpec, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>? placementSpec, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? fadeOutSpec);
+    method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public default androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
   }
 
   @androidx.compose.foundation.lazy.grid.LazyGridScopeMarker public sealed interface LazyGridItemSpanScope {
@@ -1415,10 +1416,6 @@
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.geometry.Rect calculateRectForParent(androidx.compose.ui.geometry.Rect localRect);
   }
 
-  public final class ScrollIntoView {
-    method public static suspend Object? scrollIntoView(androidx.compose.ui.node.DelegatableNode, optional androidx.compose.ui.geometry.Rect? rect, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-  }
-
 }
 
 package androidx.compose.foundation.selection {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
index 9e9a761..8d31d0c 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
@@ -20,7 +20,6 @@
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.spring
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Arrangement
@@ -58,7 +57,6 @@
 import kotlinx.coroutines.launch
 
 @Preview
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun LazyColumnDragAndDropDemo() {
     var list by remember { mutableStateOf(List(50) { it }) }
@@ -182,8 +180,7 @@
                 draggingItem.index != item.index
         }
         if (targetItem != null) {
-            if (
-                draggingItem.index == state.firstVisibleItemIndex ||
+            if (draggingItem.index == state.firstVisibleItemIndex ||
                 targetItem.index == state.firstVisibleItemIndex
             ) {
                 state.requestScrollToItem(
@@ -225,7 +222,6 @@
     }
 }
 
-@ExperimentalFoundationApi
 @Composable
 fun LazyItemScope.DraggableItem(
     dragDropState: DragDropState,
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt
index 25063b2..4f7a65a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt
@@ -22,7 +22,6 @@
 import androidx.compose.animation.core.VisibilityThreshold
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.spring
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Arrangement
@@ -64,7 +63,6 @@
 import kotlinx.coroutines.launch
 
 @Preview
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun LazyGridDragAndDropDemo() {
     var list by remember { mutableStateOf(List(50) { it }) }
@@ -91,7 +89,9 @@
                     Text(
                         "Item $item",
                         textAlign = TextAlign.Center,
-                        modifier = Modifier.fillMaxWidth().padding(vertical = 40.dp)
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(vertical = 40.dp)
                     )
                 }
             }
@@ -193,22 +193,15 @@
                 draggingItem.index != item.index
         }
         if (targetItem != null) {
-            val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
-                draggingItem.index
-            } else if (draggingItem.index == state.firstVisibleItemIndex) {
-                targetItem.index
-            } else {
-                null
+            if (draggingItem.index == state.firstVisibleItemIndex ||
+                targetItem.index == state.firstVisibleItemIndex
+            ) {
+                state.requestScrollToItem(
+                    state.firstVisibleItemIndex,
+                    state.firstVisibleItemScrollOffset
+                )
             }
-            if (scrollToIndex != null) {
-                scope.launch {
-                    // this is needed to neutralize automatic keeping the first item first.
-                    state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
-                    onMove.invoke(draggingItem.index, targetItem.index)
-                }
-            } else {
-                onMove.invoke(draggingItem.index, targetItem.index)
-            }
+            onMove.invoke(draggingItem.index, targetItem.index)
             draggingItemIndex = targetItem.index
         } else {
             val overscroll = when {
@@ -250,7 +243,6 @@
     }
 }
 
-@ExperimentalFoundationApi
 @Composable
 fun LazyGridItemScope.DraggableItem(
     dragDropState: GridDragDropState,
@@ -267,13 +259,14 @@
                 translationY = dragDropState.draggingItemOffset.y
             }
     } else if (index == dragDropState.previousIndexOfDraggedItem) {
-        Modifier.zIndex(1f)
+        Modifier
+            .zIndex(1f)
             .graphicsLayer {
                 translationX = dragDropState.previousItemOffset.value.x
                 translationY = dragDropState.previousItemOffset.value.y
             }
     } else {
-        Modifier.animateItemPlacement()
+        Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
     }
     Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) {
         content(dragging)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
index fc2c255..2ad70ef 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
@@ -33,14 +33,20 @@
 import androidx.compose.foundation.text.appendInlineContent
 import androidx.compose.foundation.text.selection.SelectionContainer
 import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
+import androidx.compose.material.TextDefaults
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.LinkInteractionListener
 import androidx.compose.ui.text.Placeholder
 import androidx.compose.ui.text.PlaceholderVerticalAlign
 import androidx.compose.ui.text.SpanStyle
@@ -55,6 +61,7 @@
 import androidx.compose.ui.text.withStyle
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.fastForEach
 
 private const val WebLink = "https://google.com"
 private const val LongWebLink =
@@ -124,17 +131,17 @@
             }
             Text(text = stringWithLink)
         }
-        Sample("Styling with Material colors") {
-            val linkColor = MaterialTheme.colors.primary
+        Sample("Material colors for links from builder") {
             Text(buildAnnotatedString {
-                append("BasicText with ")
-                withLink(LinkAnnotation.Url(WebLink, style = SpanStyle(color = linkColor))) {
-                    append("developer.android.com")
-                }
+                append("Text and ")
+                withLink(TextDefaults.Url(url = WebLink)) { append("developer.android.com") }
                 append(" link.")
             })
         }
-
+        Sample("Material colors for links from html") {
+            val htmlString = "Text and <a href=https://google.com>developer.android.com</a> link"
+            Text(TextDefaults.fromHtml(htmlString = htmlString))
+        }
         Sample("Long links") {
             val text = buildAnnotatedString {
                 append("Example that contains ")
@@ -231,6 +238,79 @@
             AnnotatedStringWithHoveredLinkStylingSample()
             AnnotatedStringWithListenerSample()
         }
+        Sample("Custom Saver with link listener restoration") {
+            val listener = LinkInteractionListener { /* do something */ }
+            val text = buildAnnotatedString {
+                withLink(TextDefaults.Clickable("tag", linkInteractionListener = listener)) {
+                    append("Click me")
+                }
+            }
+            val saveableText = rememberSaveable(stateSaver = AnnotatedStringSaver(listener)) {
+                mutableStateOf(text)
+            }
+            Text(saveableText.value)
+        }
+    }
+}
+
+private class AnnotatedStringSaver(
+    private val linkInteractionListener: LinkInteractionListener?
+) : Saver<AnnotatedString, Any> {
+    override fun SaverScope.save(value: AnnotatedString): Any? {
+        // It will store LinkAnnotation ignoring the LinkInteractionListener that needs to be put
+        // back manually after restoration
+        return with(AnnotatedString.Saver) {
+            save(value)
+        }
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Suppress("UNCHECKED_CAST")
+    @SuppressLint("NullAnnotationGroup")
+    override fun restore(value: Any): AnnotatedString? {
+       with(AnnotatedString.Saver as Saver<AnnotatedString, Any>) {
+           val result = this.restore(value)
+           result?.let { text ->
+               // create a builder and copy over all styles
+               val builder = AnnotatedString.Builder(text.text)
+               text.spanStyles.fastForEach { builder.addStyle(it.item, it.start, it.end) }
+               text.paragraphStyles.fastForEach { builder.addStyle(it.item, it.start, it.end) }
+               // put annotations back apart from links
+               text.getStringAnnotations(0, text.length).fastForEach {
+                   builder.addStringAnnotation(it.tag, it.item, it.start, it.end)
+               }
+               text.getTtsAnnotations(0, text.length).fastForEach {
+                   builder.addTtsAnnotation(it.item, it.start, it.end)
+               }
+               // copy link annotations and apply the listener where needed
+               text.getLinkAnnotations(0, text.length).fastForEach { linkRange ->
+                   when (linkRange.item) {
+                       is LinkAnnotation.Url -> {
+                           // in our example we assume that we never provide listeners to the
+                           // LinkAnnotation.Url annotations. Therefore we restore them as is.
+                           builder.addLink(
+                               linkRange.item as LinkAnnotation.Url,
+                               linkRange.start,
+                               linkRange.end
+                           )
+                       }
+                       is LinkAnnotation.Clickable -> {
+                           val link = LinkAnnotation.Clickable(
+                               (linkRange.item as LinkAnnotation.Clickable).tag,
+                               linkRange.item.style,
+                               linkRange.item.focusedStyle,
+                               linkRange.item.hoveredStyle,
+                               linkInteractionListener
+                           )
+                           builder.addLink(link, linkRange.start, linkRange.end)
+                       }
+                   }
+               }
+
+               return builder.toAnnotatedString()
+           }
+           return null
+        }
     }
 }
 
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemAppearanceAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemAppearanceAnimationTest.kt
new file mode 100644
index 0000000..f982bfd
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemAppearanceAnimationTest.kt
@@ -0,0 +1,428 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
+package androidx.compose.foundation.lazy.grid
+
+import android.os.Build
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.abs
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class LazyGridItemAppearanceAnimationTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val itemSize: Int = 4
+    private var itemSizeDp: Dp = Dp.Infinity
+    private val crossAxisSize: Int = 2
+    private var crossAxisSizeDp: Dp = Dp.Infinity
+    private val containerSize: Float = itemSize * 2f
+    private var containerSizeDp: Dp = Dp.Infinity
+    private lateinit var state: LazyGridState
+
+    @Before
+    fun before() {
+        rule.mainClock.autoAdvance = false
+        with(rule.density) {
+            itemSizeDp = itemSize.toDp()
+            crossAxisSizeDp = crossAxisSize.toDp()
+            containerSizeDp = containerSize.toDp()
+        }
+    }
+
+    @Test
+    fun oneItemAdded() {
+        var list by mutableStateOf(emptyList<Color>())
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(mainAxisSize = itemSize) { _, _ -> Color.Black.copy(alpha = fraction) }
+        }
+    }
+
+    @Test
+    fun noAnimationForInitialList() {
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp) {
+                items(listOf(Color.Black), key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPixels(itemSize) { _, _ -> Color.Black }
+    }
+
+    @Test
+    fun oneExistTwoAdded() {
+        var list by mutableStateOf(listOf(Color.Black))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp * 3) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black, Color.Red, Color.Green)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(itemSize * 3) { _, offset ->
+                when (offset) {
+                    in 0 until itemSize -> Color.Black
+                    in itemSize until itemSize * 2 -> Color.Red.copy(alpha = fraction)
+                    else -> Color.Green.copy(alpha = fraction)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun onlyItemWithSpecsIsAnimating() {
+        var list by mutableStateOf(emptyList<Color>())
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp * 2) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it, animSpec = if (it == Color.Red) AnimSpec else null)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black, Color.Red)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(itemSize * 2) { _, offset ->
+                when (offset) {
+                    in 0 until itemSize -> Color.Black
+                    else -> Color.Red.copy(alpha = fraction)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun itemAddedOutsideOfViewportIsNotAnimated() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Red, Color.Green))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp * 2) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            // Blue is added before Green, both are outside the bounds
+            list = listOf(Color.Black, Color.Red, Color.Blue, Color.Green)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                // scroll 1.5 items so we now see half of Red, Blue and half of Green
+                state.scrollBy(itemSize * 1.5f)
+            }
+        }
+
+        onAnimationFrame {
+            assertPixels(itemSize * 2) { _, offset ->
+                when (offset) {
+                    in 0 until itemSize / 2 -> Color.Red
+                    in itemSize / 2 until itemSize * 3 / 2 -> Color.Blue
+                    else -> Color.Green
+                }
+            }
+        }
+    }
+
+    @Test
+    fun animatedItemChangesTheContainerSize() {
+        var list by mutableStateOf(listOf(Color.Black))
+        rule.setContent {
+            LazyGrid(containerSize = null) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertHeightIsEqualTo(itemSizeDp)
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black, Color.Red)
+        }
+
+        onAnimationFrame {
+            rule.onNodeWithTag(ContainerTag)
+                .assertHeightIsEqualTo(itemSizeDp * 2)
+        }
+    }
+
+    @Test
+    fun removeItemBeingAnimated() {
+        var list by mutableStateOf(emptyList<Color>())
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            if (fraction < 0.5f) {
+                assertPixels(itemSize) { _, _ -> Color.Black.copy(alpha = fraction) }
+            } else {
+                if (fraction.isCloseTo(0.5f)) {
+                    rule.runOnUiThread {
+                        list = emptyList()
+                    }
+                }
+                assertPixels(itemSize) { _, _ -> Color.Transparent }
+            }
+        }
+    }
+
+    @Test
+    fun scrollAwayFromAnimatedItem() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Green, Color.Blue, Color.Yellow))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp * 2) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            // item at position 1 is new
+            list = listOf(Color.Black, Color.Red, Color.Green, Color.Blue, Color.Yellow)
+        }
+
+        onAnimationFrame { fraction ->
+            if (fraction < 0.35f) {
+                assertPixels(itemSize * 2) { _, offset ->
+                    when (offset) {
+                        in 0 until itemSize -> Color.Black
+                        else -> Color.Red.copy(alpha = fraction)
+                    }
+                }
+            } else if (fraction.isCloseTo(0.5f)) {
+                rule.runOnUiThread {
+                    runBlocking { state.scrollBy(itemSize * 2f) }
+                    runBlocking { state.scrollBy(itemSize * 0.5f) }
+                }
+                assertPixels(itemSize * 2) { _, offset ->
+                    // red item is not displayed anywhere
+                    when (offset) {
+                        in 0 until itemSize / 2 -> Color.Green
+                        in itemSize / 2 until itemSize * 3 / 2 -> Color.Blue
+                        else -> Color.Yellow
+                    }
+                }
+            } else {
+                if (fraction.isCloseTo(0.75f)) {
+                    rule.runOnUiThread {
+                        runBlocking {
+                            state.scrollBy(-itemSize * 1.5f)
+                        }
+                    }
+                }
+                assertPixels(itemSize * 2) { _, offset ->
+                    // red item is not displayed anywhere
+                    when (offset) {
+                        // the animation should be canceled so the red item has no alpha
+                        in 0 until itemSize -> Color.Red
+                        else -> Color.Green
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun theWholeLineIsAdded() {
+        var list by mutableStateOf(listOf(Color.Green, Color.Red))
+        val containerSize = itemSize * 2
+        val containerSizeDp = itemSizeDp * 2
+        rule.setContent {
+            LazyGrid(cells = 2, containerSize = containerSizeDp, crossAxisSize = containerSizeDp) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Green, Color.Red, Color.Blue, Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(containerSize, containerSize) { x, y ->
+                // red item is not displayed anywhere
+                when {
+                    // the animation should be canceled so the red item has no alpha
+                    x < itemSize && y < itemSize -> Color.Green
+                    x >= itemSize && y < itemSize -> Color.Red
+                    x < itemSize && y >= itemSize -> Color.Blue.copy(alpha = fraction)
+                    else -> Color.Black.copy(alpha = fraction)
+                }
+            }
+        }
+    }
+
+    private fun assertPixels(
+        mainAxisSize: Int,
+        crossAxisSize: Int = this.crossAxisSize,
+        expectedColorProvider: (x: Int, y: Int) -> Color?
+    ) {
+        rule.onNodeWithTag(ContainerTag)
+            .captureToImage()
+            .assertPixels(IntSize(crossAxisSize, mainAxisSize)) {
+                expectedColorProvider(it.x, it.y)?.compositeOver(Color.White)
+            }
+    }
+
+    private fun onAnimationFrame(duration: Long = Duration, onFrame: (fraction: Float) -> Unit) {
+        require(duration.mod(FrameDuration) == 0L)
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        var expectedTime = rule.mainClock.currentTime
+        for (i in 0..duration step FrameDuration) {
+            val fraction = i / duration.toFloat()
+            onFrame(fraction)
+            if (i < duration) {
+                rule.mainClock.advanceTimeBy(FrameDuration)
+                expectedTime += FrameDuration
+                assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+            }
+        }
+    }
+
+    @Composable
+    private fun LazyGrid(
+        cells: Int = 1,
+        containerSize: Dp? = containerSizeDp,
+        startIndex: Int = 0,
+        crossAxisSize: Dp = crossAxisSizeDp,
+        content: LazyGridScope.() -> Unit
+    ) {
+        state = rememberLazyGridState(startIndex)
+
+        LazyVerticalGrid(
+            GridCells.Fixed(cells),
+            state = state,
+            modifier = Modifier
+                .then(
+                    if (containerSize != null) {
+                        Modifier.requiredHeight(containerSize)
+                    } else {
+                        Modifier
+                    }
+                )
+                .background(Color.White)
+                .then(
+                    if (crossAxisSize != Dp.Unspecified) {
+                        Modifier.requiredWidth(crossAxisSize)
+                    } else {
+                        Modifier.fillMaxWidth()
+                    }
+                )
+                .testTag(ContainerTag),
+            content = content
+        )
+    }
+
+    @Composable
+    private fun LazyGridItemScope.Item(
+        color: Color,
+        size: Dp = itemSizeDp,
+        crossAxisSize: Dp = crossAxisSizeDp,
+        animSpec: FiniteAnimationSpec<Float>? = AnimSpec
+    ) {
+        Box(
+            Modifier
+                .animateItem(
+                    fadeInSpec = animSpec,
+                    placementSpec = null,
+                    fadeOutSpec = null
+                )
+                .background(color)
+                .requiredHeight(size)
+                .requiredWidth(crossAxisSize)
+        )
+    }
+}
+
+private val FrameDuration = 16L
+private val Duration = 64L // 4 frames, so we get 0f, 0.25f, 0.5f, 0.75f and 1f fractions
+private val AnimSpec = tween<Float>(Duration.toInt(), easing = LinearEasing)
+private val ContainerTag = "container"
+
+internal fun Float.isCloseTo(expected: Float) = abs(this - expected) < 0.01f
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemDisappearanceAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemDisappearanceAnimationTest.kt
new file mode 100644
index 0000000..0575235
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemDisappearanceAnimationTest.kt
@@ -0,0 +1,541 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.grid
+
+import android.os.Build
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class LazyGridItemDisappearanceAnimationTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    // the numbers should be divisible by 8 to avoid the rounding issues as we run 4 or 8 frames
+    // of the animation.
+    private val itemSize: Int = 4
+    private var itemSizeDp: Dp = Dp.Infinity
+    private val crossAxisSize: Int = 2
+    private var crossAxisSizeDp: Dp = Dp.Infinity
+    private val containerSize: Float = itemSize * 2f
+    private var containerSizeDp: Dp = Dp.Infinity
+    private lateinit var state: LazyGridState
+
+    @Before
+    fun before() {
+        rule.mainClock.autoAdvance = false
+        with(rule.density) {
+            itemSizeDp = itemSize.toDp()
+            crossAxisSizeDp = crossAxisSize.toDp()
+            containerSizeDp = containerSize.toDp()
+        }
+    }
+
+    @Test
+    fun oneItemRemoved() {
+        var list by mutableStateOf(listOf(Color.Black))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = emptyList()
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(mainAxisSize = itemSize) { _, _ ->
+                Color.Black.copy(alpha = 1f - fraction)
+            }
+        }
+    }
+
+    @Test
+    fun threeExistTwoRemoved() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Red, Color.Green))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp * 3) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(itemSize * 3) { _, offset ->
+                when (offset) {
+                    in 0 until itemSize -> Color.Black
+                    in itemSize until itemSize * 2 -> Color.Red.copy(alpha = 1f - fraction)
+                    else -> Color.Green.copy(alpha = 1f - fraction)
+                }
+            }
+        }
+    }
+
+    // todo copy with content padding and horizontal
+    @Test
+    fun threeExistTwoRemoved_reverseLayout() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Red, Color.Green))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp * 3, reverseLayout = true) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(itemSize * 3) { _, offset ->
+                when (offset) {
+                    in 0 until itemSize -> Color.Green.copy(alpha = 1f - fraction)
+                    in itemSize until itemSize * 2 -> Color.Red.copy(alpha = 1f - fraction)
+                    else -> Color.Black
+                }
+            }
+        }
+    }
+
+    @Test
+    fun oneRemoved_reverseLayout_contentPadding() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Red))
+        rule.setContent {
+            LazyGrid(
+                containerSize = itemSizeDp * 3,
+                reverseLayout = true,
+                contentPadding = PaddingValues(bottom = itemSizeDp)
+            ) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(itemSize * 3) { _, offset ->
+                when (offset) {
+                    in 0 until itemSize -> Color.Red.copy(alpha = 1f - fraction)
+                    in itemSize until itemSize * 2 -> Color.Black
+                    else -> Color.Transparent
+                }
+            }
+        }
+    }
+
+    @Test
+    fun onlyItemWithSpecsIsAnimating() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Red))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp * 2) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it, disappearanceSpec = if (it == Color.Red) AnimSpec else null)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = emptyList()
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(itemSize * 2) { _, offset ->
+                when (offset) {
+                    in 0 until itemSize -> Color.Transparent
+                    else -> Color.Red.copy(alpha = 1f - fraction)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun itemRemovedOutsideOfViewportIsNotAnimated() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Red, Color.Blue, Color.Green))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp * 2) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            // Blue is removed before Green, both are outside the bounds
+            list = listOf(Color.Black, Color.Red, Color.Green)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                // scroll 0.5 items so we now see half of Black, Red and half of Green
+                state.scrollBy(itemSize * 0.5f)
+            }
+        }
+
+        onAnimationFrame {
+            assertPixels(itemSize * 2) { _, offset ->
+                when (offset) {
+                    in 0 until itemSize / 2 -> Color.Black
+                    in itemSize / 2 until itemSize * 3 / 2 -> Color.Red
+                    else -> Color.Green
+                }
+            }
+        }
+    }
+
+    @Test
+    fun itemsBeingRemovedAreAffectingTheContainerSizeForTheDurationOfAnimation() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Red))
+        rule.setContent {
+            LazyGrid(containerSize = null) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertHeightIsEqualTo(itemSizeDp * 2)
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            val heightDp = rule.onNodeWithTag(ContainerTag)
+                .getBoundsInRoot().height
+            val heightPx = with(rule.density) { heightDp.roundToPx() }
+            Truth.assertWithMessage("Height on fraction=$fraction")
+                .that(heightPx)
+                .isEqualTo(if (fraction < 1f) itemSize * 2 else itemSize)
+
+            if (fraction < 1f) {
+                assertPixels(itemSize * 2) { _, offset ->
+                    when (offset) {
+                        in 0 until itemSize -> Color.Black
+                        else -> Color.Red.copy(1f - fraction)
+                    }
+                }
+            } else {
+                assertPixels(itemSize) { _, _ -> Color.Black }
+            }
+        }
+    }
+
+    @Test
+    fun itemsBeingRemovedAreAffectingTheContainerSizeForTheDurationOfAnimation_reverseLayout() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Red))
+        rule.setContent {
+            LazyGrid(containerSize = null, reverseLayout = true) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertHeightIsEqualTo(itemSizeDp * 2)
+
+        assertPixels(itemSize * 2) { _, offset ->
+            when (offset) {
+                in 0 until itemSize -> Color.Red
+                else -> Color.Black
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            val heightDp = rule.onNodeWithTag(ContainerTag)
+                .getBoundsInRoot().height
+            val heightPx = with(rule.density) { heightDp.roundToPx() }
+            Truth.assertWithMessage("Height on fraction=$fraction")
+                .that(heightPx)
+                .isEqualTo(if (fraction < 1f) itemSize * 2 else itemSize)
+
+            if (fraction < 1f) {
+                assertPixels(itemSize * 2) { _, offset ->
+                    when (offset) {
+                        in 0 until itemSize -> Color.Red.copy(1f - fraction)
+                        else -> Color.Black
+                    }
+                }
+            } else {
+                assertPixels(itemSize) { _, _ -> Color.Black }
+            }
+        }
+    }
+
+    @Test
+    fun reAddItemBeingAnimated_withoutAppearanceAnimation() {
+        var list by mutableStateOf(listOf(Color.Black))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = emptyList()
+        }
+
+        onAnimationFrame { fraction ->
+            if (fraction < 0.5f) {
+                assertPixels(itemSize) { _, _ -> Color.Black.copy(alpha = 1f - fraction) }
+            } else {
+                if (fraction.isCloseTo(0.5f)) {
+                    rule.runOnUiThread {
+                        list = listOf(Color.Black)
+                    }
+                }
+                assertPixels(itemSize) { _, _ -> Color.Black }
+            }
+        }
+    }
+
+    @Test
+    fun reAddItemBeingAnimated_withAppearanceAnimation() {
+        var list by mutableStateOf(listOf(Color.Black))
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it, appearanceSpec = HalfDurationAnimSpec)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = emptyList()
+        }
+
+        onAnimationFrame { fraction ->
+            if (fraction < 0.5f) {
+                assertPixels(itemSize) { _, _ -> Color.Black.copy(alpha = 1f - fraction) }
+            } else {
+                if (fraction.isCloseTo(0.5f)) {
+                    rule.runOnUiThread {
+                        list = listOf(Color.Black)
+                    }
+                }
+                assertPixels(itemSize) { _, _ ->
+                Color.Black.copy(alpha = fraction)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun removeItemBeingAnimatedForAppearance() {
+        var list by mutableStateOf(emptyList<Color>())
+        rule.setContent {
+            LazyGrid(containerSize = itemSizeDp) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it, appearanceSpec = AnimSpec, disappearanceSpec = HalfDurationAnimSpec)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = listOf(Color.Black)
+        }
+
+        onAnimationFrame { fraction ->
+            if (fraction < 0.5f) {
+                assertPixels(itemSize) { _, _ -> Color.Black.copy(alpha = fraction) }
+            } else {
+                if (fraction.isCloseTo(0.5f)) {
+                    rule.runOnUiThread {
+                        list = emptyList()
+                    }
+                }
+                assertPixels(itemSize) { _, _ ->
+                    Color.Black.copy(alpha = 1f - fraction)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun wholeLineRemoved() {
+        var list by mutableStateOf(listOf(Color.Black, Color.Green))
+        rule.setContent {
+            LazyGrid(cells = 2, containerSize = itemSizeDp, crossAxisSize = itemSizeDp * 2) {
+                items(list, key = { it.toArgb() }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnUiThread {
+            list = emptyList()
+        }
+
+        onAnimationFrame { fraction ->
+            assertPixels(mainAxisSize = itemSize, crossAxisSize = itemSize * 2) { x, _ ->
+                if (x < itemSize) {
+                    Color.Black.copy(alpha = 1f - fraction)
+                } else {
+                    Color.Green.copy(alpha = 1f - fraction)
+                }
+            }
+        }
+    }
+
+    private fun assertPixels(
+        mainAxisSize: Int,
+        crossAxisSize: Int = this.crossAxisSize,
+        expectedColorProvider: (x: Int, y: Int) -> Color?
+    ) {
+        rule.onNodeWithTag(ContainerTag)
+            .captureToImage()
+            .assertPixels(IntSize(crossAxisSize, mainAxisSize)) {
+                expectedColorProvider(it.x, it.y)?.compositeOver(Color.White)
+            }
+    }
+
+    private fun onAnimationFrame(duration: Long = Duration, onFrame: (fraction: Float) -> Unit) {
+        require(duration.mod(FrameDuration) == 0L)
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        var expectedTime = rule.mainClock.currentTime
+        for (i in 0..duration step FrameDuration) {
+            val fraction = i / duration.toFloat()
+            onFrame(fraction)
+            if (i < duration) {
+                rule.mainClock.advanceTimeBy(FrameDuration)
+                expectedTime += FrameDuration
+                Truth.assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+            }
+        }
+    }
+
+    @Composable
+    private fun LazyGrid(
+        cells: Int = 1,
+        containerSize: Dp? = containerSizeDp,
+        startIndex: Int = 0,
+        crossAxisSize: Dp = crossAxisSizeDp,
+        reverseLayout: Boolean = false,
+        contentPadding: PaddingValues = PaddingValues(0.dp),
+        content: LazyGridScope.() -> Unit
+    ) {
+        state = rememberLazyGridState(startIndex)
+
+        LazyVerticalGrid(
+            GridCells.Fixed(cells),
+            state = state,
+            modifier = Modifier.then(
+                if (containerSize != null) {
+                    Modifier.requiredHeight(containerSize)
+                } else {
+                    Modifier
+                }
+            )
+                .background(Color.White)
+                .then(
+                    if (crossAxisSize != Dp.Unspecified) {
+                        Modifier.requiredWidth(crossAxisSize)
+                    } else {
+                        Modifier.fillMaxWidth()
+                    }
+                )
+                .testTag(ContainerTag),
+            contentPadding = contentPadding,
+            reverseLayout = reverseLayout,
+            content = content
+        )
+    }
+
+    @Composable
+    private fun LazyGridItemScope.Item(
+        color: Color,
+        size: Dp = itemSizeDp,
+        crossAxisSize: Dp = crossAxisSizeDp,
+        disappearanceSpec: FiniteAnimationSpec<Float>? = AnimSpec,
+        appearanceSpec: FiniteAnimationSpec<Float>? = null
+    ) {
+        Box(
+            Modifier
+                .animateItem(
+                    fadeInSpec = appearanceSpec,
+                    placementSpec = null,
+                    fadeOutSpec = disappearanceSpec
+                )
+                .background(color)
+                .requiredHeight(size)
+                .requiredWidth(crossAxisSize)
+        )
+    }
+}
+
+private val FrameDuration = 16L
+private val Duration = 64L // 4 frames, so we get 0f, 0.25f, 0.5f, 0.75f and 1f fractions
+private val AnimSpec = tween<Float>(Duration.toInt(), easing = LinearEasing)
+private val HalfDurationAnimSpec = tween<Float>(Duration.toInt() / 2, easing = LinearEasing)
+private val ContainerTag = "container"
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimationTest.kt
similarity index 98%
rename from compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
rename to compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimationTest.kt
index fbb1cb0..403f55a 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimationTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.animation.core.VisibilityThreshold
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -65,10 +64,9 @@
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
-@OptIn(ExperimentalFoundationApi::class)
 @LargeTest
 @RunWith(Parameterized::class)
-class LazyGridAnimateItemPlacementTest(private val config: Config) {
+class LazyGridItemPlacementAnimationTest(private val config: Config) {
 
     private val isVertical: Boolean get() = config.isVertical
     private val reverseLayout: Boolean get() = config.reverseLayout
@@ -693,15 +691,15 @@
         }
 
         onAnimationFrame { fraction ->
-            // item 8 moves to and item 1 moves from `-itemSize`, right before the start edge
+            // item 8 moves to and item 1 moves from `-itemSizePlusSpacing`, right before the start edge
             val item1Offset = AxisOffset(
                 0f,
-                -itemSize + (itemSize + itemSizePlusSpacing * 2) * fraction
+                -itemSizePlusSpacing + (itemSizePlusSpacing + itemSizePlusSpacing * 2) * fraction
             )
             val item8Offset = AxisOffset(
                 0f,
                 itemSizePlusSpacing * 2 -
-                    (itemSize + itemSizePlusSpacing * 2) * fraction
+                    (itemSizePlusSpacing + itemSizePlusSpacing * 2) * fraction
             )
             val expected = mutableListOf<Pair<Any, Offset>>().apply {
                 if (item1Offset.mainAxis > -itemSize) {
@@ -2503,7 +2501,11 @@
     ) {
         Box(
             if (animSpec != null) {
-                Modifier.animateItemPlacement(animSpec)
+                Modifier.animateItem(
+                    placementSpec = animSpec,
+                    fadeInSpec = null,
+                    fadeOutSpec = null
+                )
             } else {
                 Modifier
             }
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
index d8c6a8c..61c2ef9 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.animation.core.FiniteAnimationSpec
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
@@ -62,7 +61,6 @@
 
 @LargeTest
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalFoundationApi::class)
 class LazyListItemAppearanceAnimationTest {
 
     @get:Rule
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemDisappearanceAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemDisappearanceAnimationTest.kt
index 76fa5a7..61ea124 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemDisappearanceAnimationTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemDisappearanceAnimationTest.kt
@@ -20,7 +20,6 @@
 import androidx.compose.animation.core.FiniteAnimationSpec
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
@@ -64,7 +63,6 @@
 
 @LargeTest
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalFoundationApi::class)
 class LazyListItemDisappearanceAnimationTest {
 
     @get:Rule
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
index f9a3ca6..7ea574b 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
@@ -84,7 +84,6 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-@OptIn(ExperimentalFoundationApi::class)
 class LazyListAnimateItemPlacementTest(private val config: Config) {
 
     private val isVertical: Boolean get() = config.isVertical
@@ -522,6 +521,7 @@
         }
     }
 
+    @OptIn(ExperimentalFoundationApi::class)
     @Test
     fun moveItemToTheTopOutsideOfBounds_withStickyHeader() {
         var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
index 9490b5b..5e79954 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.wrapContentSize
@@ -30,17 +31,19 @@
 import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.foundation.lazy.grid.itemsIndexed
 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.material.Button
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.flow.collect
 
 @Sampled
 @Composable
@@ -48,7 +51,10 @@
     val itemsList = (0..5).toList()
     val itemsIndexedList = listOf("A", "B", "C")
 
-    val itemModifier = Modifier.border(1.dp, Color.Blue).height(80.dp).wrapContentSize()
+    val itemModifier = Modifier
+        .border(1.dp, Color.Blue)
+        .height(80.dp)
+        .wrapContentSize()
 
     LazyVerticalGrid(
         columns = GridCells.Fixed(3)
@@ -78,7 +84,10 @@
             item(span = { GridItemSpan(maxLineSpan) }) {
                 Text(
                     "This is section $index",
-                    Modifier.border(1.dp, Color.Gray).height(80.dp).wrapContentSize()
+                    Modifier
+                        .border(1.dp, Color.Gray)
+                        .height(80.dp)
+                        .wrapContentSize()
                 )
             }
             items(
@@ -88,7 +97,10 @@
             ) {
                 Text(
                     "Item $it",
-                    Modifier.border(1.dp, Color.Blue).height(80.dp).wrapContentSize()
+                    Modifier
+                        .border(1.dp, Color.Blue)
+                        .height(80.dp)
+                        .wrapContentSize()
                 )
             }
         }
@@ -101,7 +113,10 @@
     val itemsList = (0..5).toList()
     val itemsIndexedList = listOf("A", "B", "C")
 
-    val itemModifier = Modifier.border(1.dp, Color.Blue).width(80.dp).wrapContentSize()
+    val itemModifier = Modifier
+        .border(1.dp, Color.Blue)
+        .width(80.dp)
+        .wrapContentSize()
 
     LazyHorizontalGrid(
         rows = GridCells.Fixed(3),
@@ -135,7 +150,10 @@
             item(span = { GridItemSpan(maxLineSpan) }) {
                 Text(
                     "This is section $index",
-                    Modifier.border(1.dp, Color.Gray).width(80.dp).wrapContentSize()
+                    Modifier
+                        .border(1.dp, Color.Gray)
+                        .width(80.dp)
+                        .wrapContentSize()
                 )
             }
             items(
@@ -145,7 +163,10 @@
             ) {
                 Text(
                     "Item $it",
-                    Modifier.border(1.dp, Color.Blue).width(80.dp).wrapContentSize()
+                    Modifier
+                        .border(1.dp, Color.Blue)
+                        .width(80.dp)
+                        .wrapContentSize()
                 )
             }
         }
@@ -190,6 +211,25 @@
     }
 }
 
+@Sampled
+@Composable
+fun GridAnimateItemSample() {
+    var list by remember { mutableStateOf(listOf("A", "B", "C")) }
+    Column {
+        Button(onClick = { list = list + "D" }) {
+            Text("Add new item")
+        }
+        Button(onClick = { list = list.shuffled() }) {
+            Text("Shuffle")
+        }
+        LazyVerticalGrid(columns = GridCells.Fixed(1)) {
+            items(list, key = { it }) {
+                Text("Item $it", Modifier.animateItem())
+            }
+        }
+    }
+}
+
 @Composable
 private fun ScrollToTopButton(@Suppress("UNUSED_PARAMETER") gridState: LazyGridState) {
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollScreenshotTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollScreenshotTest.kt
index 056f526..317a96a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollScreenshotTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollScreenshotTest.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -50,7 +49,6 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTestApi::class)
 class OverscrollScreenshotTest {
     @get:Rule
     val rule = createComposeRule()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TransformableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TransformableTest.kt
index f4061cff..7883b43 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TransformableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TransformableTest.kt
@@ -40,7 +40,6 @@
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -68,7 +67,6 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTestApi::class)
 class TransformableTest {
     @Suppress("DEPRECATION")
     @get:Rule
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
index 4499d46..877c0e6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
@@ -588,7 +588,6 @@
         rule.onNodeWithTag(Tag).assert(SemanticsMatcher.keyNotDefined(SemanticsActions.CopyText))
     }
 
-    @OptIn(ExperimentalTestApi::class)
     @Test
     fun semantics_copy_appliesFilter() {
         val state = TextFieldState("Hello World!", initialSelection = TextRange(0, 5))
@@ -615,7 +614,6 @@
         }
     }
 
-    @OptIn(ExperimentalTestApi::class)
     @Test
     fun semantics_cut() {
         val state = TextFieldState("Hello World!", initialSelection = TextRange(0, 5))
@@ -638,7 +636,6 @@
         }
     }
 
-    @OptIn(ExperimentalTestApi::class)
     @Test
     fun semantics_cut_appliesFilter() {
         val state = TextFieldState("Hello World!", initialSelection = TextRange(0, 5))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSendKeyEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSendKeyEventTest.kt
new file mode 100644
index 0000000..e3c3a42
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSendKeyEventTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.input
+
+import android.view.KeyCharacterMap
+import android.view.KeyEvent
+import android.view.View
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.input.internal.ComposeInputMethodManager
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class BasicTextFieldSendKeyEventTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val immRule = ComposeInputMethodManagerTestRule()
+
+    private val inputMethodInterceptor = InputMethodInterceptor(rule)
+
+    private val Tag = "BasicTextField"
+
+    @Test
+    fun sendKeyEvent_doesNotRestartInput_forTypedEvent() {
+        lateinit var imm: FakeInputMethodManager
+        lateinit var originalFactory: ((View) -> ComposeInputMethodManager)
+
+        originalFactory = immRule.setFactory {
+            val actualImm = originalFactory(it)
+            imm = object : FakeInputMethodManager() {
+                override fun sendKeyEvent(event: KeyEvent) {
+                    actualImm.sendKeyEvent(event)
+                }
+            }
+            imm
+        }
+
+        val state = TextFieldState()
+        inputMethodInterceptor.setContent {
+            BasicTextField(
+                state = state,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus()
+
+        inputMethodInterceptor.withInputConnection {
+            sendKeyEvent(softKeyEvent(KeyEvent.KEYCODE_A, KeyEventType.KeyDown))
+            sendKeyEvent(softKeyEvent(KeyEvent.KEYCODE_A, KeyEventType.KeyUp))
+        }
+
+        rule.waitForIdle()
+
+        imm.expectCall("updateSelection(1, 1, -1, -1)")
+        imm.expectNoMoreCalls()
+
+        assertThat(state.text.toString()).isEqualTo("a")
+    }
+
+    @Test
+    fun sendKeyEvent_doesNotRestartInput_forBackspaceEvent() {
+        lateinit var imm: FakeInputMethodManager
+        lateinit var originalFactory: ((View) -> ComposeInputMethodManager)
+
+        originalFactory = immRule.setFactory {
+            val actualImm = originalFactory(it)
+            imm = object : FakeInputMethodManager() {
+                override fun sendKeyEvent(event: KeyEvent) {
+                    actualImm.sendKeyEvent(event)
+                }
+            }
+            imm
+        }
+
+        val state = TextFieldState("abc")
+        inputMethodInterceptor.setContent {
+            BasicTextField(
+                state = state,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus()
+
+        inputMethodInterceptor.withInputConnection {
+            sendKeyEvent(softKeyEvent(KeyEvent.KEYCODE_DEL, KeyEventType.KeyDown))
+            sendKeyEvent(softKeyEvent(KeyEvent.KEYCODE_DEL, KeyEventType.KeyUp))
+        }
+
+        rule.waitForIdle()
+
+        imm.expectCall("updateSelection(2, 2, -1, -1)")
+        imm.expectNoMoreCalls()
+
+        assertThat(state.text.toString()).isEqualTo("ab")
+    }
+
+    private fun requestFocus() = rule.onNodeWithTag(Tag).requestFocus()
+}
+
+/**
+ * Creates a native [KeyEvent] instance that originates from IME.
+ */
+private fun softKeyEvent(keyCode: Int, keyEventType: KeyEventType): KeyEvent {
+    val action = when (keyEventType) {
+        KeyEventType.KeyDown -> KeyEvent.ACTION_DOWN
+        KeyEventType.KeyUp -> KeyEvent.ACTION_UP
+        else -> error("Unknown key event type")
+    }
+    return KeyEvent(
+        0L, 0L, action, keyCode, 0, 0,
+        KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+        KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
+    )
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/ComposeInputMethodManagerTestRule.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/ComposeInputMethodManagerTestRule.kt
index 6e6655c..baab893 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/ComposeInputMethodManagerTestRule.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/ComposeInputMethodManagerTestRule.kt
@@ -30,11 +30,20 @@
 internal class ComposeInputMethodManagerTestRule : TestRule {
     private var initialFactory: ((View) -> ComposeInputMethodManager)? = null
 
-    fun setFactory(factory: (View) -> ComposeInputMethodManager) {
+    /**
+     * Replaces the [ComposeInputMethodManager] factory with the given [factory].
+     *
+     * @return The initial factory that can be used to delegate select calls to not fully override
+     * the default [ComposeInputMethodManager].
+     */
+    fun setFactory(
+        factory: (View) -> ComposeInputMethodManager
+    ): ((View) -> ComposeInputMethodManager) {
         val previousFactory = overrideComposeInputMethodManagerFactoryForTests(factory)
         if (initialFactory == null) {
             initialFactory = previousFactory
         }
+        return initialFactory!!
     }
 
     override fun apply(base: Statement, description: Description): Statement =
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
index 989f8c9..924e5b1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
@@ -22,7 +22,7 @@
 import androidx.compose.foundation.text.input.internal.ComposeInputMethodManager
 import com.google.common.truth.Truth.assertThat
 
-internal class FakeInputMethodManager : ComposeInputMethodManager {
+internal open class FakeInputMethodManager : ComposeInputMethodManager {
     private val calls = mutableListOf<String>()
 
     fun expectCall(description: String) {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
index 7787cf8..aece3ef 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
@@ -23,7 +23,6 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -41,7 +40,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
+@OptIn(ExperimentalFoundationApi::class)
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class TextFieldOutputTransformationGesturesIntegrationTest {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
index 1907e91..063c0b8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
@@ -58,7 +58,6 @@
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.assertCountEquals
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.captureToImage
@@ -598,7 +597,6 @@
         rule.onNode(isSelectionHandle(Handle.Cursor)).assertIsDisplayed()
     }
 
-    @OptIn(ExperimentalTestApi::class)
     @Test
     fun textField_selectionHandles_hidden_whenScrolledOutOfView() {
         val size = 200
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.android.kt
index db55023..7193ee4 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.android.kt
@@ -35,6 +35,10 @@
 internal actual fun createTextFieldKeyEventHandler(): TextFieldKeyEventHandler =
     AndroidTextFieldKeyEventHandler()
 
+internal actual val KeyEvent.isFromSoftKeyboard: Boolean
+    get() = (nativeKeyEvent.flags and android.view.KeyEvent.FLAG_SOFT_KEYBOARD) ==
+        android.view.KeyEvent.FLAG_SOFT_KEYBOARD
+
 internal class AndroidTextFieldKeyEventHandler : TextFieldKeyEventHandler() {
 
     override fun onPreKeyEvent(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 20e88eb..5021fa1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -459,7 +459,13 @@
             val absScroll = if (isReversed) scroll - side else -scroll
             val xOffset = if (isVertical) 0 else absScroll
             val yOffset = if (isVertical) absScroll else 0
-            placeable.placeRelativeWithLayer(xOffset, yOffset)
+
+            // Tagging as direct manipulation, such that consumers of this offset can decide whether
+            // to exclude this offset on their coordinates calculation. Such as whether an
+            // `approachLayout` will animate it or directly apply the offset without animation.
+            withDirectManipulationPlacement {
+                placeable.placeRelativeWithLayer(xOffset, yOffset)
+            }
         }
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt
index 97d4dee..d1e3b18 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt
@@ -120,7 +120,7 @@
     @Deprecated(
         "Use Modifier.animateItem() instead",
         ReplaceWith(
-            "Modifier.animateItem(enterSpec = null, exitSpec = null, " +
+            "Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null, " +
                 "placementSpec = animationSpec)"
         )
     )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
index 3ca66ff..dd8d012 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
@@ -17,7 +17,7 @@
 package androidx.compose.foundation.lazy
 
 import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimationSpecsNode
+import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateItemElement
 import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.ui.Modifier
@@ -74,7 +74,7 @@
         if (fadeInSpec == null && placementSpec == null && fadeOutSpec == null) {
             this
         } else {
-            this then AnimateItemElement(
+            this then LazyLayoutAnimateItemElement(
                 fadeInSpec,
                 placementSpec,
                 fadeOutSpec
@@ -160,30 +160,3 @@
         }
     }
 }
-
-private data class AnimateItemElement(
-    val fadeInSpec: FiniteAnimationSpec<Float>?,
-    val placementSpec: FiniteAnimationSpec<IntOffset>?,
-    val fadeOutSpec: FiniteAnimationSpec<Float>?
-) : ModifierNodeElement<LazyLayoutAnimationSpecsNode>() {
-
-    override fun create(): LazyLayoutAnimationSpecsNode =
-        LazyLayoutAnimationSpecsNode(
-            fadeInSpec,
-            placementSpec,
-            fadeOutSpec
-        )
-
-    override fun update(node: LazyLayoutAnimationSpecsNode) {
-        node.fadeInSpec = fadeInSpec
-        node.placementSpec = placementSpec
-        node.fadeOutSpec = fadeOutSpec
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "animateItem"
-        properties["fadeInSpec"] = fadeInSpec
-        properties["placementSpec"] = placementSpec
-        properties["fadeOutSpec"] = fadeOutSpec
-    }
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index 33ccd6f..3923710 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -165,6 +165,7 @@
     verticalArrangement: Arrangement.Vertical?,
     /** Scope for animations */
     coroutineScope: CoroutineScope,
+    /** Used for creating graphics layers */
     graphicsContext: GraphicsContext,
     stickyHeadersEnabled: Boolean,
 ) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
@@ -267,7 +268,8 @@
                 index: Int,
                 key: Any,
                 contentType: Any?,
-                placeables: List<Placeable>
+                placeables: List<Placeable>,
+                constraints: Constraints
             ): LazyListMeasuredItem {
                 // we add spaceBetweenItems as an extra spacing for all items apart from the last one so
                 // the lazy list measuring logic will take it into account.
@@ -286,7 +288,8 @@
                     visualOffset = visualItemOffset,
                     key = key,
                     contentType = contentType,
-                    animator = state.itemAnimator
+                    animator = state.itemAnimator,
+                    constraints = constraints
                 )
             }
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index da56802..b5088e5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.layout.MeasureResult
@@ -59,7 +60,7 @@
     horizontalArrangement: Arrangement.Horizontal?,
     reverseLayout: Boolean,
     density: Density,
-    itemAnimator: LazyListItemAnimator,
+    itemAnimator: LazyLayoutItemAnimator<LazyListMeasuredItem>,
     beyondBoundsItemCount: Int,
     pinnedItems: List<Int>,
     hasLookaheadPassOccurred: Boolean,
@@ -81,6 +82,7 @@
             layoutWidth = layoutWidth,
             layoutHeight = layoutHeight,
             positionedItems = mutableListOf(),
+            keyIndexMap = measuredItemProvider.keyIndexMap,
             itemProvider = measuredItemProvider,
             isVertical = isVertical,
             isLookingAhead = isLookingAhead,
@@ -166,7 +168,7 @@
             val measuredItem = measuredItemProvider.getAndMeasure(previous)
             visibleItems.add(0, measuredItem)
             maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
-            currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
+            currentFirstItemScrollOffset += measuredItem.mainAxisSizeWithSpacings
             currentFirstItemIndex = previous
         }
 
@@ -193,7 +195,7 @@
                 remeasureNeeded = true
             } else {
                 index++
-                currentMainAxisOffset += visibleItems[indexInVisibleItems].sizeWithSpacings
+                currentMainAxisOffset += visibleItems[indexInVisibleItems].mainAxisSizeWithSpacings
                 indexInVisibleItems++
             }
         }
@@ -207,12 +209,12 @@
                 visibleItems.isEmpty())
         ) {
             val measuredItem = measuredItemProvider.getAndMeasure(index)
-            currentMainAxisOffset += measuredItem.sizeWithSpacings
+            currentMainAxisOffset += measuredItem.mainAxisSizeWithSpacings
 
             if (currentMainAxisOffset <= minOffset && index != itemsCount - 1) {
                 // this item is offscreen and will not be visible. advance firstVisibleItemIndex
                 currentFirstItemIndex = index + 1
-                currentFirstItemScrollOffset -= measuredItem.sizeWithSpacings
+                currentFirstItemScrollOffset -= measuredItem.mainAxisSizeWithSpacings
                 remeasureNeeded = true
             } else {
                 maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
@@ -236,7 +238,7 @@
                 val measuredItem = measuredItemProvider.getAndMeasure(previousIndex)
                 visibleItems.add(0, measuredItem)
                 maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
-                currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
+                currentFirstItemScrollOffset += measuredItem.mainAxisSizeWithSpacings
                 currentFirstItemIndex = previousIndex
             }
             scrollDelta += toScrollBack
@@ -278,7 +280,7 @@
         // located there for the state's scroll position calculation (first item + first offset)
         if (beforeContentPadding > 0 || spaceBetweenItems < 0) {
             for (i in visibleItems.indices) {
-                val size = visibleItems[i].sizeWithSpacings
+                val size = visibleItems[i].mainAxisSizeWithSpacings
                 if (currentFirstItemScrollOffset != 0 && size <= currentFirstItemScrollOffset &&
                     i != visibleItems.lastIndex
                 ) {
@@ -350,6 +352,7 @@
             layoutWidth = layoutWidth,
             layoutHeight = layoutHeight,
             positionedItems = positionedItems,
+            keyIndexMap = measuredItemProvider.keyIndexMap,
             itemProvider = measuredItemProvider,
             isVertical = isVertical,
             isLookingAhead = isLookingAhead,
@@ -488,12 +491,12 @@
                         ?: list?.fastFirstOrNull { it.index == index }
                     if (item != null) {
                         index++
-                        totalOffset += item.sizeWithSpacings
+                        totalOffset += item.mainAxisSizeWithSpacings
                     } else {
                         if (list == null) list = mutableListOf()
                         list.add(measuredItemProvider.getAndMeasure(index))
                         index++
-                        totalOffset += list.last().sizeWithSpacings
+                        totalOffset += list.last().mainAxisSizeWithSpacings
                     }
                 }
             }
@@ -618,7 +621,7 @@
     } else {
         var currentMainAxis = itemsScrollOffset
         extraItemsBefore.fastForEach {
-            currentMainAxis -= it.sizeWithSpacings
+            currentMainAxis -= it.mainAxisSizeWithSpacings
             it.position(currentMainAxis, layoutWidth, layoutHeight)
             positionedItems.add(it)
         }
@@ -627,13 +630,13 @@
         items.fastForEach {
             it.position(currentMainAxis, layoutWidth, layoutHeight)
             positionedItems.add(it)
-            currentMainAxis += it.sizeWithSpacings
+            currentMainAxis += it.mainAxisSizeWithSpacings
         }
 
         extraItemsAfter.fastForEach {
             it.position(currentMainAxis, layoutWidth, layoutHeight)
             positionedItems.add(it)
-            currentMainAxis += it.sizeWithSpacings
+            currentMainAxis += it.mainAxisSizeWithSpacings
         }
     }
     return positionedItems
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
index 1c0096e..e70556a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
@@ -89,7 +89,8 @@
     fun tryToApplyScrollWithoutRemeasure(delta: Int, updateAnimations: Boolean): Boolean {
         if (remeasureNeeded || visibleItemsInfo.isEmpty() || firstVisibleItem == null ||
             // applying this delta will change firstVisibleItem
-            (firstVisibleItemScrollOffset - delta) !in 0 until firstVisibleItem.sizeWithSpacings
+            (firstVisibleItemScrollOffset - delta) !in
+            0 until firstVisibleItem.mainAxisSizeWithSpacings
         ) {
             return false
         }
@@ -102,9 +103,9 @@
         val canApply = if (delta < 0) {
             // scrolling forward
             val deltaToFirstItemChange =
-                first.offset + first.sizeWithSpacings - viewportStartOffset
+                first.offset + first.mainAxisSizeWithSpacings - viewportStartOffset
             val deltaToLastItemChange =
-                last.offset + last.sizeWithSpacings - viewportEndOffset
+                last.offset + last.mainAxisSizeWithSpacings - viewportEndOffset
             minOf(deltaToFirstItemChange, deltaToLastItemChange) > -delta
         } else {
             // scrolling backward
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
index 80c07d7..3e71395 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
@@ -17,10 +17,13 @@
 package androidx.compose.foundation.lazy
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimation.Companion.NotInitialized
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimation.Companion.NotInitialized
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasuredItem
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.util.fastForEach
@@ -33,7 +36,7 @@
 internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor(
     override val index: Int,
     private val placeables: List<Placeable>,
-    val isVertical: Boolean,
+    override val isVertical: Boolean,
     private val horizontalAlignment: Alignment.Horizontal?,
     private val verticalAlignment: Alignment.Vertical?,
     private val layoutDirection: LayoutDirection,
@@ -52,8 +55,9 @@
     private val visualOffset: IntOffset,
     override val key: Any,
     override val contentType: Any?,
-    private val animator: LazyListItemAnimator
-) : LazyListItemInfo {
+    private val animator: LazyLayoutItemAnimator<LazyListMeasuredItem>,
+    override val constraints: Constraints
+) : LazyListItemInfo, LazyLayoutMeasuredItem {
     override var offset: Int = 0
         private set
 
@@ -63,9 +67,14 @@
     override val size: Int
 
     /**
+     * In lists we have one item per line.
+     */
+    override val line: Int = index
+
+    /**
      * Sum of the main axis sizes of all the inner placeables and [spacing].
      */
-    val sizeWithSpacings: Int
+    override val mainAxisSizeWithSpacings: Int
 
     /**
      * Max of the cross axis sizes of all the inner placeables.
@@ -76,7 +85,7 @@
      * True when this item is not supposted to react on scroll delta. for example sticky header,
      * or items being animated away out of the bounds are non scrollable.
      */
-    var nonScrollableItem: Boolean = false
+    override var nonScrollableItem: Boolean = false
 
     private var mainAxisLayoutSize: Int = Unset
     private var minMainAxisOffset: Int = 0
@@ -94,27 +103,41 @@
             maxCrossAxis = maxOf(maxCrossAxis, if (!isVertical) it.height else it.width)
         }
         size = mainAxisSize
-        sizeWithSpacings = (size + spacing).coerceAtLeast(0)
+        mainAxisSizeWithSpacings = (size + spacing).coerceAtLeast(0)
         crossAxisSize = maxCrossAxis
         placeableOffsets = IntArray(placeables.size * 2)
     }
 
-    val placeablesCount: Int get() = placeables.size
+    override val placeablesCount: Int get() = placeables.size
 
-    fun getParentData(index: Int) = placeables[index].parentData
+    override fun getParentData(index: Int) = placeables[index].parentData
 
-    /**
-     * Calculates positions for the inner placeables at [offset] main axis position.
-     * If [reverseOrder] is true the inner placeables would be placed in the inverted order.
-     */
-    fun position(
-        offset: Int,
+    override fun position(
+        mainAxisOffset: Int,
+        crossAxisOffset: Int,
         layoutWidth: Int,
         layoutHeight: Int
     ) {
-        this.offset = offset
+        require(crossAxisOffset == 0) {
+            "positioning a list item with non zero crossAxisOffset is not supported." +
+                "$crossAxisOffset was passed."
+        }
+        position(mainAxisOffset, layoutWidth, layoutHeight)
+    }
+
+    /**
+     * Calculates positions for the inner placeables at [mainAxisOffset] main axis position.
+     * If [reverseOrder] is true the inner placeables would be placed in the inverted order.
+     */
+    fun position(
+        mainAxisOffset: Int,
+        layoutWidth: Int,
+        layoutHeight: Int
+    ) {
+        this.offset = mainAxisOffset
         mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
-        var mainAxisOffset = offset
+        @Suppress("NAME_SHADOWING")
+        var mainAxisOffset = mainAxisOffset
         placeables.fastForEachIndexed { index, placeable ->
             val indexInArray = index * 2
             if (isVertical) {
@@ -146,7 +169,7 @@
         maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
     }
 
-    fun getOffset(index: Int) =
+    override fun getOffset(index: Int) =
         IntOffset(placeableOffsets[index * 2], placeableOffsets[index * 2 + 1])
 
     fun applyScrollDelta(delta: Int, updateAnimations: Boolean) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt
index 0fe137d..9ed162d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
 import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasuredItemProvider
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
 
@@ -31,22 +32,24 @@
     isVertical: Boolean,
     private val itemProvider: LazyListItemProvider,
     private val measureScope: LazyLayoutMeasureScope
-) {
+) : LazyLayoutMeasuredItemProvider<LazyListMeasuredItem> {
     // the constraints we will measure child with. the main axis is not restricted
     val childConstraints = Constraints(
         maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity,
         maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity
     )
 
+    fun getAndMeasure(index: Int): LazyListMeasuredItem = getAndMeasure(index, childConstraints)
+
     /**
      * Used to subcompose items of lazy lists. Composed placeables will be measured with the
      * correct constraints and wrapped into [LazyListMeasuredItem].
      */
-    fun getAndMeasure(index: Int): LazyListMeasuredItem {
+    override fun getAndMeasure(index: Int, constraints: Constraints): LazyListMeasuredItem {
         val key = itemProvider.getKey(index)
         val contentType = itemProvider.getContentType(index)
-        val placeables = measureScope.measure(index, childConstraints)
-        return createItem(index, key, contentType, placeables)
+        val placeables = measureScope.measure(index, constraints)
+        return createItem(index, key, contentType, placeables, constraints)
     }
 
     /**
@@ -59,6 +62,7 @@
         index: Int,
         key: Any,
         contentType: Any?,
-        placeables: List<Placeable>
+        placeables: List<Placeable>,
+        constraints: Constraints
     ): LazyListMeasuredItem
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 49d1258..55a8676 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -34,6 +34,7 @@
 import androidx.compose.foundation.lazy.LazyListState.Companion.Saver
 import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
@@ -261,7 +262,7 @@
      */
     internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
 
-    internal val itemAnimator = LazyListItemAnimator()
+    internal val itemAnimator = LazyLayoutItemAnimator<LazyListMeasuredItem>()
 
     internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index fdcaa80..7ef0a3e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -36,8 +36,10 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.platform.LocalGraphicsContext
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
@@ -79,6 +81,7 @@
     val semanticState = rememberLazyGridSemanticState(state, reverseLayout)
 
     val coroutineScope = rememberCoroutineScope()
+    val graphicsContext = LocalGraphicsContext.current
     val measurePolicy = rememberLazyGridMeasurePolicy(
         itemProviderLambda,
         state,
@@ -88,7 +91,8 @@
         isVertical,
         horizontalArrangement,
         verticalArrangement,
-        coroutineScope
+        coroutineScope,
+        graphicsContext
     )
 
     val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
@@ -111,6 +115,7 @@
                 orientation = orientation,
                 enabled = userScrollEnabled
             )
+            .then(state.itemAnimator.modifier)
             .scrollingContainer(
                 state = state,
                 orientation = orientation,
@@ -151,7 +156,9 @@
     /** The vertical arrangement for items */
     verticalArrangement: Arrangement.Vertical?,
     /** Coroutine scope for item animations */
-    coroutineScope: CoroutineScope
+    coroutineScope: CoroutineScope,
+    /** Used for creating graphics layers */
+    graphicsContext: GraphicsContext
 ) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
     state,
     slots,
@@ -160,6 +167,7 @@
     isVertical,
     horizontalArrangement,
     verticalArrangement,
+    graphicsContext
 ) {
     { containerConstraints ->
         state.measurementScopeInvalidator.attachToScope()
@@ -246,7 +254,8 @@
                 contentType: Any?,
                 crossAxisSize: Int,
                 mainAxisSpacing: Int,
-                placeables: List<Placeable>
+                placeables: List<Placeable>,
+                constraints: Constraints
             ) = LazyGridMeasuredItem(
                 index = index,
                 key = key,
@@ -260,7 +269,9 @@
                 visualOffset = visualItemOffset,
                 placeables = placeables,
                 contentType = contentType,
-                animator = state.placementAnimator
+                animator = state.itemAnimator,
+                spanLayoutProvider = spanLayoutProvider,
+                constraints = constraints
             )
         }
         val measuredLineProvider = object : LazyGridMeasuredLineProvider(
@@ -340,12 +351,13 @@
                 horizontalArrangement = horizontalArrangement,
                 reverseLayout = reverseLayout,
                 density = this,
-                placementAnimator = state.placementAnimator,
-                spanLayoutProvider = spanLayoutProvider,
+                itemAnimator = state.itemAnimator,
+                slotsPerLine = slotsPerLine,
                 pinnedItems = pinnedItems,
                 coroutineScope = coroutineScope,
                 placementScopeInvalidator = state.placementScopeInvalidator,
                 prefetchInfoRetriever = prefetchInfoRetriever,
+                graphicsContext = graphicsContext,
                 layout = { width, height, placement ->
                     layout(
                         containerConstraints.constrainWidth(width + totalHorizontalPadding),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
deleted file mode 100644
index 3cbc6fd..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ /dev/null
@@ -1,367 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.grid
-
-import androidx.collection.mutableScatterMapOf
-import androidx.collection.mutableScatterSetOf
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimation
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimationSpecsNode
-import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-import kotlinx.coroutines.CoroutineScope
-
-/**
- * Handles the item placement animations when it is set via [LazyGridItemScope.animateItemPlacement].
- *
- * This class is responsible for detecting when item position changed, figuring our start/end
- * offsets and starting the animations.
- */
-internal class LazyGridItemPlacementAnimator {
-    // state containing relevant info for active items.
-    private val keyToItemInfoMap = mutableScatterMapOf<Any, ItemInfo>()
-
-    // snapshot of the key to index map used for the last measuring.
-    private var keyIndexMap: LazyLayoutKeyIndexMap = LazyLayoutKeyIndexMap.Empty
-
-    // keeps the index of the first visible item index.
-    private var firstVisibleIndex = 0
-
-    // stored to not allocate it every pass.
-    private val movingAwayKeys = mutableScatterSetOf<Any>()
-    private val movingInFromStartBound = mutableListOf<LazyGridMeasuredItem>()
-    private val movingInFromEndBound = mutableListOf<LazyGridMeasuredItem>()
-    private val movingAwayToStartBound = mutableListOf<LazyGridMeasuredItem>()
-    private val movingAwayToEndBound = mutableListOf<LazyGridMeasuredItem>()
-
-    /**
-     * Should be called after the measuring so we can detect position changes and start animations.
-     *
-     * Note that this method can compose new item and add it into the [positionedItems] list.
-     */
-    fun onMeasured(
-        consumedScroll: Int,
-        layoutWidth: Int,
-        layoutHeight: Int,
-        positionedItems: MutableList<LazyGridMeasuredItem>,
-        itemProvider: LazyGridMeasuredItemProvider,
-        spanLayoutProvider: LazyGridSpanLayoutProvider,
-        isVertical: Boolean,
-        coroutineScope: CoroutineScope
-    ) {
-        if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
-            // no animations specified - no work needed
-            reset()
-            return
-        }
-
-        val previousFirstVisibleIndex = firstVisibleIndex
-        firstVisibleIndex = positionedItems.firstOrNull()?.index ?: 0
-        val previousKeyToIndexMap = keyIndexMap
-        keyIndexMap = itemProvider.keyIndexMap
-
-        val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
-
-        // the consumed scroll is considered as a delta we don't need to animate
-        val scrollOffset = if (isVertical) {
-            IntOffset(0, consumedScroll)
-        } else {
-            IntOffset(consumedScroll, 0)
-        }
-
-        // first add all items we had in the previous run
-        keyToItemInfoMap.forEachKey { movingAwayKeys.add(it) }
-        // iterate through the items which are visible (without animated offsets)
-        positionedItems.fastForEach { item ->
-            // remove items we have in the current one as they are still visible.
-            movingAwayKeys.remove(item.key)
-            if (item.hasAnimations) {
-                val itemInfo = keyToItemInfoMap[item.key]
-                // there is no state associated with this item yet
-                if (itemInfo == null) {
-                    val newItemInfo = ItemInfo(item.crossAxisSize, item.crossAxisOffset)
-                    newItemInfo.updateAnimation(item, coroutineScope)
-                    keyToItemInfoMap[item.key] = newItemInfo
-                    val previousIndex = previousKeyToIndexMap.getIndex(item.key)
-                    if (previousIndex != -1 && item.index != previousIndex) {
-                        if (previousIndex < previousFirstVisibleIndex) {
-                            // the larger index will be in the start of the list
-                            movingInFromStartBound.add(item)
-                        } else {
-                            movingInFromEndBound.add(item)
-                        }
-                    } else {
-                        initializeAnimation(
-                            item,
-                            item.offset.let { if (item.isVertical) it.y else it.x },
-                            newItemInfo
-                        )
-                    }
-                } else {
-                    itemInfo.animations.forEach { animation ->
-                        if (animation != null &&
-                            animation.rawOffset != LazyLayoutAnimation.NotInitialized
-                        ) {
-                            animation.rawOffset += scrollOffset
-                        }
-                    }
-                    itemInfo.crossAxisSize = item.crossAxisSize
-                    itemInfo.crossAxisOffset = item.crossAxisOffset
-                    startAnimationsIfNeeded(item)
-                }
-            } else {
-                // no animation, clean up if needed
-                keyToItemInfoMap.remove(item.key)
-            }
-        }
-
-        var accumulatedOffset = 0
-        var previousLine = -1
-        var previousLineMainAxisSize = 0
-        movingInFromStartBound.sortByDescending { previousKeyToIndexMap.getIndex(it.key) }
-        movingInFromStartBound.fastForEach { item ->
-            val line = if (isVertical) item.row else item.column
-            if (line != -1 && line == previousLine) {
-                previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
-            } else {
-                accumulatedOffset += previousLineMainAxisSize
-                previousLineMainAxisSize = item.mainAxisSize
-                previousLine = line
-            }
-            val mainAxisOffset = 0 - accumulatedOffset - item.mainAxisSize
-            initializeAnimation(item, mainAxisOffset)
-            startAnimationsIfNeeded(item)
-        }
-        accumulatedOffset = 0
-        previousLine = -1
-        previousLineMainAxisSize = 0
-        movingInFromEndBound.sortBy { previousKeyToIndexMap.getIndex(it.key) }
-        movingInFromEndBound.fastForEach { item ->
-            val line = if (isVertical) item.row else item.column
-            if (line != -1 && line == previousLine) {
-                previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
-            } else {
-                accumulatedOffset += previousLineMainAxisSize
-                previousLineMainAxisSize = item.mainAxisSize
-                previousLine = line
-            }
-            val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
-            initializeAnimation(item, mainAxisOffset)
-            startAnimationsIfNeeded(item)
-        }
-
-        movingAwayKeys.forEach { key ->
-            // found an item which was in our map previously but is not a part of the
-            // positionedItems now
-            val itemInfo = keyToItemInfoMap[key]!!
-            val newIndex = keyIndexMap.getIndex(key)
-
-            if (newIndex == -1) {
-                keyToItemInfoMap.remove(key)
-            } else {
-                val item = itemProvider.getAndMeasure(
-                    newIndex,
-                    constraints = if (isVertical) {
-                        Constraints.fixedWidth(itemInfo.crossAxisSize)
-                    } else {
-                        Constraints.fixedHeight(itemInfo.crossAxisSize)
-                    }
-                )
-                item.nonScrollableItem = true
-                // check if we have any active placement animation on the item
-                val inProgress =
-                    itemInfo.animations.any { it?.isPlacementAnimationInProgress == true }
-                if ((!inProgress && newIndex == previousKeyToIndexMap.getIndex(key))) {
-                    keyToItemInfoMap.remove(key)
-                } else {
-                    if (newIndex < firstVisibleIndex) {
-                        movingAwayToStartBound.add(item)
-                    } else {
-                        movingAwayToEndBound.add(item)
-                    }
-                }
-            }
-        }
-
-        accumulatedOffset = 0
-        previousLine = -1
-        previousLineMainAxisSize = 0
-        movingAwayToStartBound.sortByDescending { keyIndexMap.getIndex(it.key) }
-        movingAwayToStartBound.fastForEach { item ->
-            val line = spanLayoutProvider.getLineIndexOfItem(item.index)
-            if (line != -1 && line == previousLine) {
-                previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
-            } else {
-                accumulatedOffset += previousLineMainAxisSize
-                previousLineMainAxisSize = item.mainAxisSize
-                previousLine = line
-            }
-            val mainAxisOffset = 0 - accumulatedOffset - item.mainAxisSize
-
-            val itemInfo = keyToItemInfoMap[item.key]!!
-
-            item.position(
-                mainAxisOffset = mainAxisOffset,
-                crossAxisOffset = itemInfo.crossAxisOffset,
-                layoutWidth = layoutWidth,
-                layoutHeight = layoutHeight
-            )
-            positionedItems.add(item)
-            startAnimationsIfNeeded(item)
-        }
-        accumulatedOffset = 0
-        previousLine = -1
-        previousLineMainAxisSize = 0
-        movingAwayToEndBound.sortBy { keyIndexMap.getIndex(it.key) }
-        movingAwayToEndBound.fastForEach { item ->
-            val line = spanLayoutProvider.getLineIndexOfItem(item.index)
-            if (line != -1 && line == previousLine) {
-                previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
-            } else {
-                accumulatedOffset += previousLineMainAxisSize
-                previousLineMainAxisSize = item.mainAxisSize
-                previousLine = line
-            }
-            val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
-
-            val itemInfo = keyToItemInfoMap[item.key]!!
-            item.position(
-                mainAxisOffset = mainAxisOffset,
-                crossAxisOffset = itemInfo.crossAxisOffset,
-                layoutWidth = layoutWidth,
-                layoutHeight = layoutHeight,
-            )
-
-            positionedItems.add(item)
-            startAnimationsIfNeeded(item)
-        }
-
-        movingInFromStartBound.clear()
-        movingInFromEndBound.clear()
-        movingAwayToStartBound.clear()
-        movingAwayToEndBound.clear()
-        movingAwayKeys.clear()
-    }
-
-    /**
-     * Should be called when the animations are not needed for the next positions change,
-     * for example when we snap to a new position.
-     */
-    fun reset() {
-        keyToItemInfoMap.clear()
-        keyIndexMap = LazyLayoutKeyIndexMap.Empty
-        firstVisibleIndex = -1
-    }
-
-    private fun initializeAnimation(
-        item: LazyGridMeasuredItem,
-        mainAxisOffset: Int,
-        itemInfo: ItemInfo = keyToItemInfoMap[item.key]!!
-    ) {
-        val firstPlaceableOffset = item.offset
-
-        val targetFirstPlaceableOffset = if (item.isVertical) {
-            firstPlaceableOffset.copy(y = mainAxisOffset)
-        } else {
-            firstPlaceableOffset.copy(x = mainAxisOffset)
-        }
-
-        // initialize offsets
-        itemInfo.animations.forEach { animation ->
-            if (animation != null) {
-                val diffToFirstPlaceableOffset =
-                    item.offset - firstPlaceableOffset
-                animation.rawOffset = targetFirstPlaceableOffset + diffToFirstPlaceableOffset
-            }
-        }
-    }
-
-    private fun startAnimationsIfNeeded(item: LazyGridMeasuredItem) {
-        val itemInfo = keyToItemInfoMap[item.key]!!
-        itemInfo.animations.forEach { animation ->
-            if (animation != null) {
-                val newTarget = item.offset
-                val currentTarget = animation.rawOffset
-                if (currentTarget != LazyLayoutAnimation.NotInitialized &&
-                    currentTarget != newTarget
-                ) {
-                    animation.animatePlacementDelta(newTarget - currentTarget)
-                }
-                animation.rawOffset = newTarget
-            }
-        }
-    }
-
-    fun getAnimation(key: Any, placeableIndex: Int): LazyLayoutAnimation? {
-        return if (keyToItemInfoMap.isEmpty()) {
-            null
-        } else {
-            keyToItemInfoMap[key]?.animations?.get(placeableIndex)
-        }
-    }
-
-    private val LazyGridMeasuredItem.hasAnimations: Boolean
-        get() {
-            repeat(placeablesCount) { index ->
-                getParentData(index).specs?.let {
-                    // found at least one
-                    return true
-                }
-            }
-            return false
-        }
-}
-
-private class ItemInfo(
-    var crossAxisSize: Int,
-    var crossAxisOffset: Int
-) {
-    /**
-     * This array will have the same amount of elements as there are placeables on the item.
-     * If the element is not null this means there are specs associated with the given placeable.
-     */
-    var animations = EmptyArray
-        private set
-
-    fun updateAnimation(positionedItem: LazyGridMeasuredItem, coroutineScope: CoroutineScope) {
-        for (i in positionedItem.placeablesCount until animations.size) {
-            animations[i]?.release()
-        }
-        if (animations.size != positionedItem.placeablesCount) {
-            animations = animations.copyOf(positionedItem.placeablesCount)
-        }
-        repeat(positionedItem.placeablesCount) { index ->
-            val specs = positionedItem.getParentData(index).specs
-            if (specs == null) {
-                animations[index]?.release()
-                animations[index] = null
-            } else {
-                val item = animations[index] ?: LazyLayoutAnimation(coroutineScope).also {
-                    animations[index] = it
-                }
-                item.fadeInSpec = specs.fadeInSpec
-                item.placementSpec = specs.placementSpec
-            }
-        }
-    }
-}
-
-private val Any?.specs get() = this as? LazyLayoutAnimationSpecsNode
-
-private val EmptyArray = emptyArray<LazyLayoutAnimation?>()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScope.kt
index 236845a..1b3ba4e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScope.kt
@@ -32,6 +32,33 @@
 @LazyGridScopeMarker
 sealed interface LazyGridItemScope {
     /**
+     * This modifier animates the item appearance (fade in), disappearance (fade out) and placement
+     * changes (such as an item reordering).
+     *
+     * You should also provide a key via [LazyGridScope.item]/[LazyGridScope.items] for this
+     * modifier to enable animations.
+     *
+     * @sample androidx.compose.foundation.samples.GridAnimateItemSample
+     *
+     * @param fadeInSpec an animation specs to use for animating the item appearance.
+     * When null is provided the item will be appearing without animations.
+     * @param placementSpec an animation specs that will be used to animate the item placement.
+     * Aside from item reordering all other position changes caused by events like arrangement or
+     * alignment changes will also be animated. When null is provided no animations will happen.
+     * @param fadeOutSpec an animation specs to use for animating the item disappearance.
+     * When null is provided the item will be disappearance without animations.
+     */
+    fun Modifier.animateItem(
+        fadeInSpec: FiniteAnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
+        placementSpec: FiniteAnimationSpec<IntOffset>? = spring(
+            stiffness = Spring.StiffnessMediumLow,
+            visibilityThreshold = IntOffset.VisibilityThreshold
+        ),
+        fadeOutSpec: FiniteAnimationSpec<Float>? =
+            spring(stiffness = Spring.StiffnessMediumLow),
+    ): Modifier
+
+    /**
      * This modifier animates the item placement within the Lazy grid.
      *
      * When you provide a key via [LazyGridScope.item]/[LazyGridScope.items] this modifier will
@@ -40,11 +67,22 @@
      *
      * @param animationSpec a finite animation that will be used to animate the item placement.
      */
+    @Deprecated(
+        "Use Modifier.animateItem() instead",
+        ReplaceWith(
+            "Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null, " +
+                "placementSpec = animationSpec)"
+        )
+    )
     @ExperimentalFoundationApi
     fun Modifier.animateItemPlacement(
         animationSpec: FiniteAnimationSpec<IntOffset> = spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = IntOffset.VisibilityThreshold
         )
-    ): Modifier
+    ): Modifier = animateItem(
+        fadeInSpec = null,
+        placementSpec = animationSpec,
+        fadeOutSpec = null
+    )
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
index 04803cc..bc28eed 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
@@ -17,33 +17,23 @@
 package androidx.compose.foundation.lazy.grid
 
 import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimationSpecsNode
+import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateItemElement
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.unit.IntOffset
 
-@OptIn(ExperimentalFoundationApi::class)
 internal object LazyGridItemScopeImpl : LazyGridItemScope {
-    @ExperimentalFoundationApi
-    override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
-        this then AnimateItemElement(animationSpec)
-}
-
-private data class AnimateItemElement(
-    val placementSpec: FiniteAnimationSpec<IntOffset>
-) : ModifierNodeElement<LazyLayoutAnimationSpecsNode>() {
-
-    override fun create(): LazyLayoutAnimationSpecsNode =
-        LazyLayoutAnimationSpecsNode(null, placementSpec, null)
-
-    override fun update(node: LazyLayoutAnimationSpecsNode) {
-        node.placementSpec = placementSpec
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "animateItemPlacement"
-        value = placementSpec
-    }
+    override fun Modifier.animateItem(
+        fadeInSpec: FiniteAnimationSpec<Float>?,
+        placementSpec: FiniteAnimationSpec<IntOffset>?,
+        fadeOutSpec: FiniteAnimationSpec<Float>?
+    ): Modifier =
+        if (fadeInSpec == null && placementSpec == null && fadeOutSpec == null) {
+            this
+        } else {
+            this then LazyLayoutAnimateItemElement(
+                fadeInSpec,
+                placementSpec,
+                fadeOutSpec
+            )
+        }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 727160a..b8a983f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -19,11 +19,14 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
+import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 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.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
@@ -59,11 +62,12 @@
     horizontalArrangement: Arrangement.Horizontal?,
     reverseLayout: Boolean,
     density: Density,
-    placementAnimator: LazyGridItemPlacementAnimator,
-    spanLayoutProvider: LazyGridSpanLayoutProvider,
+    itemAnimator: LazyLayoutItemAnimator<LazyGridMeasuredItem>,
+    slotsPerLine: Int,
     pinnedItems: List<Int>,
     coroutineScope: CoroutineScope,
     placementScopeInvalidator: ObservableScopeInvalidator,
+    graphicsContext: GraphicsContext,
     prefetchInfoRetriever: (line: Int) -> List<Pair<Int, Constraints>>,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyGridMeasureResult {
@@ -71,12 +75,32 @@
     require(afterContentPadding >= 0) { "negative afterContentPadding" }
     if (itemsCount <= 0) {
         // empty data set. reset the current scroll and report zero size
+        var layoutWidth = constraints.minWidth
+        var layoutHeight = constraints.minHeight
+        itemAnimator.onMeasured(
+            consumedScroll = 0,
+            layoutWidth = layoutWidth,
+            layoutHeight = layoutHeight,
+            positionedItems = mutableListOf(),
+            keyIndexMap = measuredItemProvider.keyIndexMap,
+            itemProvider = measuredItemProvider,
+            isVertical = isVertical,
+            isLookingAhead = false,
+            hasLookaheadOccurred = false,
+            coroutineScope = coroutineScope,
+            graphicsContext = graphicsContext
+        )
+        val disappearingItemsSize = itemAnimator.minSizeToFitDisappearingItems
+        if (disappearingItemsSize != IntSize.Zero) {
+            layoutWidth = constraints.constrainWidth(disappearingItemsSize.width)
+            layoutHeight = constraints.constrainHeight(disappearingItemsSize.height)
+        }
         return LazyGridMeasureResult(
             firstVisibleLine = null,
             firstVisibleLineScrollOffset = 0,
             canScrollForward = false,
             consumedScroll = 0f,
-            measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+            measureResult = layout(layoutWidth, layoutHeight) {},
             visibleItemsInfo = emptyList(),
             viewportStartOffset = -beforeContentPadding,
             viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
@@ -87,7 +111,7 @@
             mainAxisItemSpacing = spaceBetweenLines,
             remeasureNeeded = false,
             density = density,
-            slotsPerLine = spanLayoutProvider.slotsPerLine,
+            slotsPerLine = slotsPerLine,
             coroutineScope = coroutineScope,
             prefetchInfoRetriever = prefetchInfoRetriever
         )
@@ -263,12 +287,12 @@
             }
         }
 
-        val layoutWidth = if (isVertical) {
+        var layoutWidth = if (isVertical) {
             constraints.maxWidth
         } else {
             constraints.constrainWidth(currentMainAxisOffset)
         }
-        val layoutHeight = if (isVertical) {
+        var layoutHeight = if (isVertical) {
             constraints.constrainHeight(currentMainAxisOffset)
         } else {
             constraints.maxHeight
@@ -290,17 +314,35 @@
             density = density
         )
 
-        placementAnimator.onMeasured(
+        itemAnimator.onMeasured(
             consumedScroll = consumedScroll.toInt(),
             layoutWidth = layoutWidth,
             layoutHeight = layoutHeight,
             positionedItems = positionedItems,
+            keyIndexMap = measuredItemProvider.keyIndexMap,
             itemProvider = measuredItemProvider,
-            spanLayoutProvider = spanLayoutProvider,
             isVertical = isVertical,
-            coroutineScope = coroutineScope
+            isLookingAhead = false,
+            hasLookaheadOccurred = false,
+            coroutineScope = coroutineScope,
+            graphicsContext = graphicsContext
         )
 
+        val disappearingItemsSize = itemAnimator.minSizeToFitDisappearingItems
+        if (disappearingItemsSize != IntSize.Zero) {
+            val oldMainAxisSize = if (isVertical) layoutHeight else layoutWidth
+            layoutWidth =
+                constraints.constrainWidth(maxOf(layoutWidth, disappearingItemsSize.width))
+            layoutHeight =
+                constraints.constrainHeight(maxOf(layoutHeight, disappearingItemsSize.height))
+            val newMainAxisSize = if (isVertical) layoutHeight else layoutWidth
+            if (newMainAxisSize != oldMainAxisSize) {
+                positionedItems.fastForEach {
+                    it.updateMainAxisLayoutSize(newMainAxisSize)
+                }
+            }
+        }
+
         return LazyGridMeasureResult(
             firstVisibleLine = firstLine,
             firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
@@ -328,7 +370,7 @@
             mainAxisItemSpacing = spaceBetweenLines,
             remeasureNeeded = remeasureNeeded,
             density = density,
-            slotsPerLine = spanLayoutProvider.slotsPerLine,
+            slotsPerLine = slotsPerLine,
             coroutineScope = coroutineScope,
             prefetchInfoRetriever = prefetchInfoRetriever
         )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
index d7f4dd5..0fac406 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
@@ -16,7 +16,11 @@
 
 package androidx.compose.foundation.lazy.grid
 
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasuredItem
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
@@ -29,7 +33,7 @@
 internal class LazyGridMeasuredItem(
     override val index: Int,
     override val key: Any,
-    val isVertical: Boolean,
+    override val isVertical: Boolean,
     /**
      * Cross axis size is the same for all [placeables]. Take it as parameter for the case when
      * [placeables] is empty.
@@ -47,8 +51,10 @@
      */
     private val visualOffset: IntOffset,
     override val contentType: Any?,
-    private val animator: LazyGridItemPlacementAnimator
-) : LazyGridItemInfo {
+    private val animator: LazyLayoutItemAnimator<LazyGridMeasuredItem>,
+    private val spanLayoutProvider: LazyGridSpanLayoutProvider,
+    override val constraints: Constraints
+) : LazyGridItemInfo, LazyLayoutMeasuredItem {
     /**
      * Main axis size of the item - the max main axis size of the placeables.
      */
@@ -57,15 +63,20 @@
     /**
      * The max main axis size of the placeables plus mainAxisSpacing.
      */
-    val mainAxisSizeWithSpacings: Int
+    override val mainAxisSizeWithSpacings: Int
 
-    val placeablesCount: Int get() = placeables.size
+    override val placeablesCount: Int get() = placeables.size
 
     private var mainAxisLayoutSize: Int = Unset
     private var minMainAxisOffset: Int = 0
     private var maxMainAxisOffset: Int = 0
 
-    fun getParentData(index: Int) = placeables[index].parentData
+    override fun getParentData(index: Int) = placeables[index].parentData
+
+    override val line: Int get() {
+        val line = if (isVertical) row else column
+        return if (line != -1) line else spanLayoutProvider.getLineIndexOfItem(index)
+    }
 
     init {
         var maxMainAxis = 0
@@ -89,11 +100,29 @@
     override var column: Int = LazyGridItemInfo.UnknownColumn
         private set
 
+    override fun getOffset(index: Int): IntOffset = offset
+
     /**
      * True when this item is not supposed to react on scroll delta. for example items being
      * animated away out of the bounds are non scrollable.
      */
-    var nonScrollableItem: Boolean = false
+    override var nonScrollableItem: Boolean = false
+
+    override fun position(
+        mainAxisOffset: Int,
+        crossAxisOffset: Int,
+        layoutWidth: Int,
+        layoutHeight: Int
+    ) {
+        position(
+            mainAxisOffset,
+            crossAxisOffset,
+            layoutWidth,
+            layoutHeight,
+            LazyGridItemInfo.UnknownRow,
+            LazyGridItemInfo.UnknownColumn
+        )
+    }
 
     /**
      * Calculates positions for the inner placeables at [mainAxisOffset], [crossAxisOffset].
@@ -107,8 +136,8 @@
         crossAxisOffset: Int,
         layoutWidth: Int,
         layoutHeight: Int,
-        row: Int = LazyGridItemInfo.UnknownRow,
-        column: Int = LazyGridItemInfo.UnknownColumn
+        row: Int,
+        column: Int
     ) {
         mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
         val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
@@ -129,6 +158,15 @@
         maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
     }
 
+    /**
+     * Update a [mainAxisLayoutSize] when the size did change after last [position] call.
+     * Knowing the final size is important for calculating the final position in reverse layout.
+     */
+    fun updateMainAxisLayoutSize(mainAxisLayoutSize: Int) {
+        this.mainAxisLayoutSize = mainAxisLayoutSize
+        maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
+    }
+
     fun applyScrollDelta(delta: Int) {
         if (nonScrollableItem) {
             return
@@ -153,6 +191,7 @@
 
             var offset = offset
             val animation = animator.getAnimation(key, index)
+            val layer: GraphicsLayer?
             if (animation != null) {
                 val animatedOffset = offset + animation.placementDelta
                 // cancel the animation if current and target offsets are both out of the bounds.
@@ -162,6 +201,9 @@
                     animation.cancelPlacementAnimation()
                 }
                 offset = animatedOffset
+                layer = animation.layer
+            } else {
+                layer = null
             }
             if (reverseLayout) {
                 offset = offset.copy { mainAxisOffset ->
@@ -169,10 +211,19 @@
                 }
             }
             offset += visualOffset
+            animation?.finalOffset = offset
             if (isVertical) {
-                placeable.placeWithLayer(offset)
+                if (layer != null) {
+                    placeable.placeWithLayer(offset, layer)
+                } else {
+                    placeable.placeWithLayer(offset)
+                }
             } else {
-                placeable.placeRelativeWithLayer(offset)
+                if (layer != null) {
+                    placeable.placeRelativeWithLayer(offset, layer)
+                } else {
+                    placeable.placeRelativeWithLayer(offset)
+                }
             }
         }
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
index f38e802..04ff7163 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
 import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasuredItemProvider
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
 
@@ -30,15 +31,18 @@
     private val itemProvider: LazyGridItemProvider,
     private val measureScope: LazyLayoutMeasureScope,
     private val defaultMainAxisSpacing: Int
-) {
+) : LazyLayoutMeasuredItemProvider<LazyGridMeasuredItem> {
+    override fun getAndMeasure(index: Int, constraints: Constraints): LazyGridMeasuredItem =
+        getAndMeasure(index, constraints, defaultMainAxisSpacing)
+
     /**
      * Used to subcompose individual items of lazy grids. Composed placeables will be measured
      * with the provided [constraints] and wrapped into [LazyGridMeasuredItem].
      */
     fun getAndMeasure(
         index: Int,
-        mainAxisSpacing: Int = defaultMainAxisSpacing,
-        constraints: Constraints
+        constraints: Constraints,
+        mainAxisSpacing: Int
     ): LazyGridMeasuredItem {
         val key = itemProvider.getKey(index)
         val contentType = itemProvider.getContentType(index)
@@ -55,7 +59,8 @@
             contentType,
             crossAxisSize,
             mainAxisSpacing,
-            placeables
+            placeables,
+            constraints
         )
     }
 
@@ -71,6 +76,7 @@
         contentType: Any?,
         crossAxisSize: Int,
         mainAxisSpacing: Int,
-        placeables: List<Placeable>
+        placeables: List<Placeable>,
+        constraints: Constraints
     ): LazyGridMeasuredItem
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt
index 6bbd299..f7e84f5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt
@@ -77,8 +77,8 @@
             val constraints = childConstraints(startSlot, span)
             measuredItemProvider.getAndMeasure(
                 lineConfiguration.firstItemIndex + it,
-                mainAxisSpacing,
-                constraints
+                constraints,
+                mainAxisSpacing
             ).also { startSlot += span }
         }
         return createLine(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index a15a8e0..ab042b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -27,6 +27,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
@@ -219,7 +220,7 @@
      */
     internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
 
-    internal val placementAnimator = LazyGridItemPlacementAnimator()
+    internal val itemAnimator = LazyLayoutItemAnimator<LazyGridMeasuredItem>()
 
     internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
 
@@ -297,7 +298,7 @@
         // reset previously known item positions as we don't want offset changes to be animated.
         // this offset should be considered as a scroll, not the placement change.
         if (positionChanged) {
-            placementAnimator.reset()
+            itemAnimator.reset()
         }
         scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset)
         if (forceRemeasure) {
@@ -306,7 +307,6 @@
             measurementScopeInvalidator.invalidateScope()
         }
     }
-
     /**
      * Call this function to take control of scrolling and gain the ability to send scroll events
      * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimation.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimation.kt
similarity index 89%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimation.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimation.kt
index 4fc1333..cb2e4ba 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimation.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimation.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,14 +29,16 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.ParentDataModifierNode
+import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
-internal class LazyLayoutAnimation(
+internal class LazyLayoutItemAnimation(
     private val coroutineScope: CoroutineScope,
     private val graphicsContext: GraphicsContext? = null,
     private val onLayerPropertyChanged: () -> Unit = {}
@@ -251,6 +253,33 @@
     }
 }
 
+internal data class LazyLayoutAnimateItemElement(
+    private val fadeInSpec: FiniteAnimationSpec<Float>?,
+    private val placementSpec: FiniteAnimationSpec<IntOffset>?,
+    private val fadeOutSpec: FiniteAnimationSpec<Float>?
+) : ModifierNodeElement<LazyLayoutAnimationSpecsNode>() {
+
+    override fun create(): LazyLayoutAnimationSpecsNode =
+        LazyLayoutAnimationSpecsNode(
+            fadeInSpec,
+            placementSpec,
+            fadeOutSpec
+        )
+
+    override fun update(node: LazyLayoutAnimationSpecsNode) {
+        node.fadeInSpec = fadeInSpec
+        node.placementSpec = placementSpec
+        node.fadeOutSpec = fadeOutSpec
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "animateItem"
+        properties["fadeInSpec"] = fadeInSpec
+        properties["placementSpec"] = placementSpec
+        properties["fadeOutSpec"] = fadeOutSpec
+    }
+}
+
 internal class LazyLayoutAnimationSpecsNode(
     var fadeInSpec: FiniteAnimationSpec<Float>?,
     var placementSpec: FiniteAnimationSpec<IntOffset>?,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
similarity index 74%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemAnimator.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
index 20e7149..471ccfd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.foundation.lazy
+package androidx.compose.foundation.lazy.layout
 
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimation
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimationSpecsNode
-import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
+import androidx.collection.mutableScatterMapOf
+import androidx.collection.mutableScatterSetOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -28,6 +27,7 @@
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.invalidateDraw
 import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastAny
@@ -35,7 +35,7 @@
 import kotlinx.coroutines.CoroutineScope
 
 /**
- * Handles the item animations when it is set via [LazyItemScope.animateItem].
+ * Handles the item animations when it is set via "animateItem" modifiers.
  *
  * This class is responsible for:
  * - animating item appearance for the new items.
@@ -43,9 +43,9 @@
  * animations for placement animations.
  * - animating item disappearance for the removed items.
  */
-internal class LazyListItemAnimator {
+internal class LazyLayoutItemAnimator<T : LazyLayoutMeasuredItem> {
     // state containing relevant info for active items.
-    private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
+    private val keyToItemInfoMap = mutableScatterMapOf<Any, ItemInfo>()
 
     // snapshot of the key to index map used for the last measuring.
     private var keyIndexMap: LazyLayoutKeyIndexMap? = null
@@ -54,12 +54,12 @@
     private var firstVisibleIndex = 0
 
     // stored to not allocate it every pass.
-    private val movingAwayKeys = LinkedHashSet<Any>()
-    private val movingInFromStartBound = mutableListOf<LazyListMeasuredItem>()
-    private val movingInFromEndBound = mutableListOf<LazyListMeasuredItem>()
-    private val movingAwayToStartBound = mutableListOf<LazyListMeasuredItem>()
-    private val movingAwayToEndBound = mutableListOf<LazyListMeasuredItem>()
-    private val disappearingItems = mutableListOf<LazyLayoutAnimation>()
+    private val movingAwayKeys = mutableScatterSetOf<Any>()
+    private val movingInFromStartBound = mutableListOf<T>()
+    private val movingInFromEndBound = mutableListOf<T>()
+    private val movingAwayToStartBound = mutableListOf<T>()
+    private val movingAwayToEndBound = mutableListOf<T>()
+    private val disappearingItems = mutableListOf<LazyLayoutItemAnimation>()
     private var displayingNode: DrawModifierNode? = null
 
     /**
@@ -71,16 +71,16 @@
         consumedScroll: Int,
         layoutWidth: Int,
         layoutHeight: Int,
-        positionedItems: MutableList<LazyListMeasuredItem>,
-        itemProvider: LazyListMeasuredItemProvider,
+        positionedItems: MutableList<T>,
+        keyIndexMap: LazyLayoutKeyIndexMap,
+        itemProvider: LazyLayoutMeasuredItemProvider<T>,
         isVertical: Boolean,
         isLookingAhead: Boolean,
         hasLookaheadOccurred: Boolean,
         coroutineScope: CoroutineScope,
         graphicsContext: GraphicsContext
     ) {
-        val previousKeyToIndexMap = keyIndexMap
-        val keyIndexMap = itemProvider.keyIndexMap
+        val previousKeyToIndexMap = this.keyIndexMap
         this.keyIndexMap = keyIndexMap
 
         val hasAnimations = positionedItems.fastAny { it.hasAnimations }
@@ -106,7 +106,7 @@
         // means lookahead pass, or regular pass when not in a lookahead scope.
         val shouldSetupAnimation = isLookingAhead || !hasLookaheadOccurred
         // first add all items we had in the previous run
-        movingAwayKeys.addAll(keyToItemInfoMap.keys)
+        keyToItemInfoMap.forEachKey { movingAwayKeys.add(it) }
         // iterate through the items which are visible (without animated offsets)
         positionedItems.fastForEach { item ->
             // remove items we have in the current one as they are still visible.
@@ -144,7 +144,7 @@
                         itemInfo.updateAnimation(item, coroutineScope, graphicsContext)
                         itemInfo.animations.forEach { animation ->
                             if (animation != null &&
-                                animation.rawOffset != LazyLayoutAnimation.NotInitialized
+                                animation.rawOffset != LazyLayoutItemAnimation.NotInitialized
                             ) {
                                 animation.rawOffset += scrollOffset
                             }
@@ -170,19 +170,39 @@
         }
 
         var accumulatedOffset = 0
+        var previousLine = -1
+        var previousLineMainAxisSize = 0
         if (shouldSetupAnimation && previousKeyToIndexMap != null) {
             movingInFromStartBound.sortByDescending { previousKeyToIndexMap.getIndex(it.key) }
             movingInFromStartBound.fastForEach { item ->
-                accumulatedOffset += item.sizeWithSpacings
-                val mainAxisOffset = 0 - accumulatedOffset
+                val line = item.line
+                if (line != -1 && line == previousLine) {
+                    previousLineMainAxisSize =
+                        maxOf(previousLineMainAxisSize, item.mainAxisSizeWithSpacings)
+                } else {
+                    accumulatedOffset += previousLineMainAxisSize
+                    previousLineMainAxisSize = item.mainAxisSizeWithSpacings
+                    previousLine = line
+                }
+                val mainAxisOffset = 0 - accumulatedOffset - item.mainAxisSizeWithSpacings
                 initializeAnimation(item, mainAxisOffset)
                 startPlacementAnimationsIfNeeded(item)
             }
             accumulatedOffset = 0
+            previousLine = -1
+            previousLineMainAxisSize = 0
             movingInFromEndBound.sortBy { previousKeyToIndexMap.getIndex(it.key) }
             movingInFromEndBound.fastForEach { item ->
+                val line = item.line
+                if (line != -1 && line == previousLine) {
+                    previousLineMainAxisSize =
+                        maxOf(previousLineMainAxisSize, item.mainAxisSizeWithSpacings)
+                } else {
+                    accumulatedOffset += previousLineMainAxisSize
+                    previousLineMainAxisSize = item.mainAxisSizeWithSpacings
+                    previousLine = line
+                }
                 val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
-                accumulatedOffset += item.sizeWithSpacings
                 initializeAnimation(item, mainAxisOffset)
                 startPlacementAnimationsIfNeeded(item)
             }
@@ -191,10 +211,10 @@
         movingAwayKeys.forEach { key ->
             // found an item which was in our map previously but is not a part of the
             // positionedItems now
+            val info = keyToItemInfoMap[key]!!
             val newIndex = keyIndexMap.getIndex(key)
 
             if (newIndex == -1) {
-                val info = keyToItemInfoMap.getValue(key)
                 var isProgress = false
                 info.animations.forEachIndexed { index, animation ->
                     if (animation != null) {
@@ -224,12 +244,14 @@
                     removeInfoForKey(key)
                 }
             } else {
-                val item = itemProvider.getAndMeasure(newIndex)
+                val item = itemProvider.getAndMeasure(
+                    newIndex,
+                    constraints = info.constraints!!
+                )
                 item.nonScrollableItem = true
-                val itemInfo = keyToItemInfoMap.getValue(key)
                 // check if we have any active placement animation on the item
                 val inProgress =
-                    itemInfo.animations.any { it?.isPlacementAnimationInProgress == true }
+                    info.animations.any { it?.isPlacementAnimationInProgress == true }
                 if ((!inProgress && newIndex == previousKeyToIndexMap?.getIndex(key))) {
                     removeInfoForKey(key)
                 } else {
@@ -243,30 +265,67 @@
         }
 
         accumulatedOffset = 0
+        previousLine = -1
+        previousLineMainAxisSize = 0
         movingAwayToStartBound.sortByDescending { keyIndexMap.getIndex(it.key) }
         movingAwayToStartBound.fastForEach { item ->
-            accumulatedOffset += item.sizeWithSpacings
-            val mainAxisOffset = if (isLookingAhead) {
-                positionedItems.first().offset - accumulatedOffset
+            val line = item.line
+            if (line != -1 && line == previousLine) {
+                previousLineMainAxisSize =
+                    maxOf(previousLineMainAxisSize, item.mainAxisSizeWithSpacings)
             } else {
-                0 - accumulatedOffset
+                accumulatedOffset += previousLineMainAxisSize
+                previousLineMainAxisSize = item.mainAxisSizeWithSpacings
+                previousLine = line
             }
-            item.position(mainAxisOffset, layoutWidth, layoutHeight)
+            val mainAxisOffset = if (isLookingAhead) {
+                positionedItems.first().mainAxisOffset
+            } else {
+                0
+            } - accumulatedOffset - item.mainAxisSizeWithSpacings
+
+            val itemInfo = keyToItemInfoMap[item.key]!!
+
+            item.position(
+                mainAxisOffset = mainAxisOffset,
+                crossAxisOffset = itemInfo.crossAxisOffset,
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight
+            )
             if (shouldSetupAnimation) {
                 startPlacementAnimationsIfNeeded(item)
             }
         }
 
         accumulatedOffset = 0
+        previousLine = -1
+        previousLineMainAxisSize = 0
         movingAwayToEndBound.sortBy { keyIndexMap.getIndex(it.key) }
         movingAwayToEndBound.fastForEach { item ->
+            val line = item.line
+            if (line != -1 && line == previousLine) {
+                previousLineMainAxisSize =
+                    maxOf(previousLineMainAxisSize, item.mainAxisSizeWithSpacings)
+            } else {
+                accumulatedOffset += previousLineMainAxisSize
+                previousLineMainAxisSize = item.mainAxisSizeWithSpacings
+                previousLine = line
+            }
             val mainAxisOffset = if (isLookingAhead)
-                positionedItems.last().let { it.offset + it.sizeWithSpacings } + accumulatedOffset
-            else
-                mainAxisLayoutSize + accumulatedOffset
-            accumulatedOffset += item.sizeWithSpacings
+                positionedItems.last()
+                    .let { it.mainAxisOffset + it.mainAxisSizeWithSpacings }
+            else {
+                mainAxisLayoutSize
+            } + accumulatedOffset
 
-            item.position(mainAxisOffset, layoutWidth, layoutHeight)
+            val itemInfo = keyToItemInfoMap[item.key]!!
+            item.position(
+                mainAxisOffset = mainAxisOffset,
+                crossAxisOffset = itemInfo.crossAxisOffset,
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight,
+            )
+
             if (shouldSetupAnimation) {
                 startPlacementAnimationsIfNeeded(item)
             }
@@ -296,9 +355,9 @@
      */
     fun reset() {
         if (keyToItemInfoMap.isNotEmpty()) {
-            keyToItemInfoMap.forEach {
-                it.value.animations.forEach {
-                    it?.release()
+            keyToItemInfoMap.forEachValue {
+                it.animations.forEach { animation ->
+                    animation?.release()
                 }
             }
             keyToItemInfoMap.clear()
@@ -308,9 +367,9 @@
     }
 
     private fun initializeAnimation(
-        item: LazyListMeasuredItem,
+        item: T,
         mainAxisOffset: Int,
-        itemInfo: ItemInfo = keyToItemInfoMap.getValue(item.key)
+        itemInfo: ItemInfo = keyToItemInfoMap[item.key]!!
     ) {
         val firstPlaceableOffset = item.getOffset(0)
 
@@ -330,13 +389,13 @@
         }
     }
 
-    private fun startPlacementAnimationsIfNeeded(item: LazyListMeasuredItem) {
-        val itemInfo = keyToItemInfoMap.getValue(item.key)
+    private fun startPlacementAnimationsIfNeeded(item: T) {
+        val itemInfo = keyToItemInfoMap[item.key]!!
         itemInfo.animations.forEachIndexed { placeableIndex, animation ->
             if (animation != null) {
                 val newTarget = item.getOffset(placeableIndex)
                 val currentTarget = animation.rawOffset
-                if (currentTarget != LazyLayoutAnimation.NotInitialized &&
+                if (currentTarget != LazyLayoutItemAnimation.NotInitialized &&
                     currentTarget != newTarget
                 ) {
                     animation.animatePlacementDelta(newTarget - currentTarget)
@@ -346,7 +405,7 @@
         }
     }
 
-    fun getAnimation(key: Any, placeableIndex: Int): LazyLayoutAnimation? =
+    fun getAnimation(key: Any, placeableIndex: Int): LazyLayoutItemAnimation? =
         keyToItemInfoMap[key]?.animations?.get(placeableIndex)
 
     val minSizeToFitDisappearingItems: IntSize get() {
@@ -365,7 +424,7 @@
 
     val modifier: Modifier = DisplayingDisappearingItemsElement(this)
 
-    private val LazyListMeasuredItem.hasAnimations: Boolean
+    private val T.hasAnimations: Boolean
         get() {
             repeat(placeablesCount) { index ->
                 getParentData(index).specs?.let {
@@ -376,6 +435,11 @@
             return false
         }
 
+    private val LazyLayoutMeasuredItem.mainAxisOffset
+        get() = getOffset(0).let { if (isVertical) it.y else it.x }
+    private val LazyLayoutMeasuredItem.crossAxisOffset
+        get() = getOffset(0).let { if (!isVertical) it.y else it.x }
+
     private inner class ItemInfo {
         /**
          * This array will have the same amount of elements as there are placeables on the item.
@@ -384,8 +448,11 @@
         var animations = EmptyArray
             private set
 
+        var constraints: Constraints? = null
+        var crossAxisOffset: Int = 0
+
         fun updateAnimation(
-            positionedItem: LazyListMeasuredItem,
+            positionedItem: T,
             coroutineScope: CoroutineScope,
             graphicsContext: GraphicsContext
         ) {
@@ -395,13 +462,15 @@
             if (animations.size != positionedItem.placeablesCount) {
                 animations = animations.copyOf(positionedItem.placeablesCount)
             }
+            constraints = positionedItem.constraints
+            crossAxisOffset = positionedItem.crossAxisOffset
             repeat(positionedItem.placeablesCount) { index ->
                 val specs = positionedItem.getParentData(index).specs
                 if (specs == null) {
                     animations[index]?.release()
                     animations[index] = null
                 } else {
-                    val animation = animations[index] ?: LazyLayoutAnimation(
+                    val animation = animations[index] ?: LazyLayoutItemAnimation(
                         coroutineScope = coroutineScope,
                         graphicsContext = graphicsContext,
                         // until b/329417380 is fixed we have to trigger any invalidation in
@@ -419,7 +488,7 @@
     }
 
     private data class DisplayingDisappearingItemsElement(
-        private val animator: LazyListItemAnimator
+        private val animator: LazyLayoutItemAnimator<*>
     ) : ModifierNodeElement<DisplayingDisappearingItemsNode>() {
         override fun create() = DisplayingDisappearingItemsNode(animator)
 
@@ -433,7 +502,7 @@
     }
 
     private data class DisplayingDisappearingItemsNode(
-        private var animator: LazyListItemAnimator
+        private var animator: LazyLayoutItemAnimator<*>
     ) : Modifier.Node(), DrawModifierNode {
         override fun ContentDrawScope.draw() {
             animator.disappearingItems.fastForEach {
@@ -455,7 +524,7 @@
             animator.reset()
         }
 
-        fun setAnimator(animator: LazyListItemAnimator) {
+        fun setAnimator(animator: LazyLayoutItemAnimator<*>) {
             if (this.animator != animator) {
                 if (node.isAttached) {
                     this.animator.reset()
@@ -469,4 +538,4 @@
 
 private val Any?.specs get() = this as? LazyLayoutAnimationSpecsNode
 
-private val EmptyArray = emptyArray<LazyLayoutAnimation?>()
+private val EmptyArray = emptyArray<LazyLayoutItemAnimation?>()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasuredItem.kt
new file mode 100644
index 0000000..0896634
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasuredItem.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+
+internal interface LazyLayoutMeasuredItem {
+    val line: Int
+    val index: Int
+    val key: Any
+    val isVertical: Boolean
+    val mainAxisSizeWithSpacings: Int
+    val placeablesCount: Int
+    var nonScrollableItem: Boolean
+    val constraints: Constraints
+    fun getOffset(index: Int): IntOffset
+    fun position(mainAxisOffset: Int, crossAxisOffset: Int, layoutWidth: Int, layoutHeight: Int)
+    fun getParentData(index: Int): Any?
+}
+
+internal interface LazyLayoutMeasuredItemProvider<T : LazyLayoutMeasuredItem> {
+    fun getAndMeasure(index: Int, constraints: Constraints): T
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt
index d2539b8..75e8d36 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt
@@ -18,8 +18,8 @@
 
 import androidx.collection.mutableScatterMapOf
 import androidx.collection.mutableScatterSetOf
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimation
 import androidx.compose.foundation.lazy.layout.LazyLayoutAnimationSpecsNode
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimation
 import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.util.fastAny
@@ -116,7 +116,7 @@
                 } else {
                     itemInfo.animations.forEach { animation ->
                         if (animation != null &&
-                            animation.rawOffset != LazyLayoutAnimation.NotInitialized
+                            animation.rawOffset != LazyLayoutItemAnimation.NotInitialized
                         ) {
                             animation.rawOffset += scrollOffset
                         }
@@ -255,7 +255,7 @@
             if (animation != null) {
                 val newTarget = item.offset
                 val currentTarget = animation.rawOffset
-                if (currentTarget != LazyLayoutAnimation.NotInitialized &&
+                if (currentTarget != LazyLayoutItemAnimation.NotInitialized &&
                     currentTarget != newTarget
                 ) {
                     animation.animatePlacementDelta(newTarget - currentTarget)
@@ -265,7 +265,7 @@
         }
     }
 
-    fun getAnimation(key: Any, placeableIndex: Int): LazyLayoutAnimation? {
+    fun getAnimation(key: Any, placeableIndex: Int): LazyLayoutItemAnimation? {
         return if (keyToItemInfoMap.isEmpty()) {
             null
         } else {
@@ -313,7 +313,7 @@
                 animations[index]?.release()
                 animations[index] = null
             } else {
-                val item = animations[index] ?: LazyLayoutAnimation(coroutineScope).also {
+                val item = animations[index] ?: LazyLayoutItemAnimation(coroutineScope).also {
                     animations[index] = it
                 }
                 item.fadeInSpec = specs.fadeInSpec
@@ -325,4 +325,4 @@
 
 private val Any?.specs get() = this as? LazyLayoutAnimationSpecsNode
 
-private val EmptyArray = emptyArray<LazyLayoutAnimation?>()
+private val EmptyArray = emptyArray<LazyLayoutItemAnimation?>()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
index da62957..708d78b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
@@ -40,7 +40,8 @@
  * @sample androidx.compose.foundation.samples.BringIntoViewSample
  * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
  */
-suspend fun DelegatableNode.scrollIntoView(rect: Rect? = null) {
+// TODO(b/333421581) Make public.
+internal suspend fun DelegatableNode.scrollIntoView(rect: Rect? = null) {
     if (!node.isAttached) return
     val layoutCoordinates = requireLayoutCoordinates()
     val parent = findBringIntoViewParent() ?: return
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.kt
index 79d34cf..69710621 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.kt
@@ -41,6 +41,11 @@
 internal expect fun createTextFieldKeyEventHandler(): TextFieldKeyEventHandler
 
 /**
+ * Returns whether this key event is created by the software keyboard.
+ */
+internal expect val KeyEvent.isFromSoftKeyboard: Boolean
+
+/**
  * Handles KeyEvents coming to a BasicTextField. This is mostly to support hardware keyboard but
  * any KeyEvent can also be sent by the IME or other platform systems.
  *
@@ -87,7 +92,9 @@
             if (codePoint != null) {
                 val text = StringBuilder(2).appendCodePointX(codePoint).toString()
                 return if (editable) {
-                    textFieldState.editUntransformedTextAsUser {
+                    textFieldState.editUntransformedTextAsUser(
+                        restartImeIfContentChanges = !event.isFromSoftKeyboard
+                    ) {
                         commitComposition()
                         commitText(text, 1)
                     }
@@ -104,7 +111,7 @@
             return false
         }
         var consumed = true
-        preparedSelectionContext(textFieldState, textLayoutState) {
+        preparedSelectionContext(textFieldState, textLayoutState, event.isFromSoftKeyboard) {
             when (command) {
                 KeyCommand.COPY -> textFieldSelectionState.copy(false)
                 KeyCommand.PASTE -> textFieldSelectionState.paste()
@@ -226,6 +233,7 @@
     private inline fun preparedSelectionContext(
         state: TransformedTextFieldState,
         textLayoutState: TextLayoutState,
+        isFromSoftKeyboard: Boolean,
         block: TextFieldPreparedSelection.() -> Unit
     ) {
         val layoutResult = textLayoutState.layoutResult ?: return
@@ -233,6 +241,7 @@
         val preparedSelection = TextFieldPreparedSelection(
             state = state,
             textLayoutResult = layoutResult,
+            isFromSoftKeyboard = isFromSoftKeyboard,
             visibleTextLayoutHeight = visibleTextLayoutHeight,
             textPreparedSelectionState = preparedSelectionState
         )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
index 990e981..15ccd9d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
@@ -218,9 +218,14 @@
     fun replaceText(
         newText: CharSequence,
         range: TextRange,
-        undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible
+        undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
+        restartImeIfContentChanges: Boolean = true
     ) {
-        textFieldState.editAsUser(inputTransformation, undoBehavior = undoBehavior) {
+        textFieldState.editAsUser(
+            inputTransformation = inputTransformation,
+            undoBehavior = undoBehavior,
+            restartImeIfContentChanges = restartImeIfContentChanges
+        ) {
             val selection = mapFromTransformed(range)
             replace(
                 selection.min,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextPreparedSelection.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextPreparedSelection.kt
index 3942384..8187614 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextPreparedSelection.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextPreparedSelection.kt
@@ -75,6 +75,8 @@
  * through transformed coordinates.
  * @param textLayoutResult Visual representation of text inside [state]. Used to calculate line
  * and paragraph metrics.
+ * @param isFromSoftKeyboard Whether the source event that created this selection context is coming
+ * from the IME.
  * @param visibleTextLayoutHeight Height of the visible area of text inside TextField to decide
  * where cursor needs to move when page up/down is requested.
  * @param textPreparedSelectionState An object that holds any context that needs to be long lived
@@ -85,6 +87,7 @@
 internal class TextFieldPreparedSelection(
     private val state: TransformedTextFieldState,
     private val textLayoutResult: TextLayoutResult,
+    private val isFromSoftKeyboard: Boolean,
     private val visibleTextLayoutHeight: Float,
     private val textPreparedSelectionState: TextFieldPreparedSelectionState
 ) {
@@ -112,9 +115,19 @@
      */
     inline fun deleteIfSelectedOr(block: () -> TextRange?) {
         if (!selection.collapsed) {
-            state.replaceText("", selection)
+            state.replaceText(
+                newText = "",
+                range = selection,
+                restartImeIfContentChanges = !isFromSoftKeyboard
+            )
         } else {
-            block()?.let { state.replaceText("", it) }
+            block()?.let {
+                state.replaceText(
+                    newText = "",
+                    range = it,
+                    restartImeIfContentChanges = !isFromSoftKeyboard
+                )
+            }
         }
     }
 
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt
index b9da0aa9..7f7a0c4 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt
@@ -16,7 +16,12 @@
 
 package androidx.compose.foundation.text.input.internal
 
+import androidx.compose.ui.input.key.KeyEvent
+
 /**
  * Factory function to create a platform specific [TextFieldKeyEventHandler].
  */
 internal actual fun createTextFieldKeyEventHandler() = object : TextFieldKeyEventHandler() {}
+
+internal actual val KeyEvent.isFromSoftKeyboard: Boolean
+    get() = false
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
index 8a0761c..a41bd6a 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
@@ -40,7 +40,6 @@
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventType
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.TouchInjectionScope
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
@@ -63,7 +62,6 @@
 import org.junit.Test
 
 @Suppress("WrapUnaryOperator")
-@OptIn(ExperimentalTestApi::class)
 class ScrollbarTest {
     @get:Rule
     val rule = createComposeRule()
diff --git a/compose/material/material/api/api_lint.ignore b/compose/material/material/api/api_lint.ignore
index 21f1bf6..a5065a1 100644
--- a/compose/material/material/api/api_lint.ignore
+++ b/compose/material/material/api/api_lint.ignore
@@ -1,4 +1,12 @@
 // Baseline format: 1.0
+ExecutorRegistration: androidx.compose.material.TextDefaults#Clickable(String, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.LinkInteractionListener):
+    Registration methods should have overload that accepts delivery Executor: `Clickable`
+ExecutorRegistration: androidx.compose.material.TextDefaults#Url(String, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.LinkInteractionListener):
+    Registration methods should have overload that accepts delivery Executor: `Url`
+ExecutorRegistration: androidx.compose.material.TextDefaults#fromHtml(String, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.LinkInteractionListener):
+    Registration methods should have overload that accepts delivery Executor: `fromHtml`
+
+
 KotlinDefaultParameterOrder: androidx.compose.material.SwipeToDismissKt#SwipeToDismiss(androidx.compose.material.DismissState, androidx.compose.ui.Modifier, java.util.Set<? extends androidx.compose.material.DismissDirection>, kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissDirection,? extends androidx.compose.material.ThresholdConfig>, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>) parameter #1:
     Parameter `modifier` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
 KotlinDefaultParameterOrder: androidx.compose.material.SwipeToDismissKt#SwipeToDismiss(androidx.compose.material.DismissState, androidx.compose.ui.Modifier, java.util.Set<? extends androidx.compose.material.DismissDirection>, kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissDirection,? extends androidx.compose.material.ThresholdConfig>, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>) parameter #2:
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 6b52d49..c5902c2 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -887,6 +887,13 @@
     method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.material.TabPosition>,kotlin.Unit> indicator, optional kotlin.jvm.functions.Function0<kotlin.Unit> divider, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
   }
 
+  public final class TextDefaults {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.LinkAnnotation.Clickable Clickable(String tag, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.LinkAnnotation.Url Url(String url, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, optional androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.AnnotatedString fromHtml(String htmlString, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, optional androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    field public static final androidx.compose.material.TextDefaults INSTANCE;
+  }
+
   @androidx.compose.runtime.Stable public interface TextFieldColors {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> cursorColor(boolean isError);
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 6b52d49..c5902c2 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -887,6 +887,13 @@
     method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.material.TabPosition>,kotlin.Unit> indicator, optional kotlin.jvm.functions.Function0<kotlin.Unit> divider, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
   }
 
+  public final class TextDefaults {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.LinkAnnotation.Clickable Clickable(String tag, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.LinkAnnotation.Url Url(String url, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, optional androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.AnnotatedString fromHtml(String htmlString, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, optional androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    field public static final androidx.compose.material.TextDefaults INSTANCE;
+  }
+
   @androidx.compose.runtime.Stable public interface TextFieldColors {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> cursorColor(boolean isError);
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 74e1ccf..97ec8c8 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -42,11 +42,11 @@
                 implementation(libs.kotlinStdlibCommon)
                 api("androidx.compose.animation:animation-core:1.6.0")
                 api(project(":compose:foundation:foundation"))
+                api(project(":compose:ui:ui-text"))
                 api(project(":compose:material:material-icons-core"))
                 api(project(":compose:material:material-ripple"))
                 api(project(":compose:runtime:runtime"))
                 api("androidx.compose.ui:ui:1.6.0")
-                api("androidx.compose.ui:ui-text:1.6.0")
 
                 implementation(project(":compose:animation:animation-core"))
                 implementation("androidx.compose.animation:animation:1.6.0")
diff --git a/compose/material/material/lint-baseline.xml b/compose/material/material/lint-baseline.xml
index 7d01edba..d11a145 100644
--- a/compose/material/material/lint-baseline.xml
+++ b/compose/material/material/lint-baseline.xml
@@ -119,6 +119,24 @@
     </issue>
 
     <issue
+        id="ComposableNaming"
+        message="Composable functions with a return type should start with a lowercase letter"
+        errorLine1="    fun Url("
+        errorLine2="        ~~~">
+        <location
+            file="src/commonMain/kotlin/androidx/compose/material/Text.kt"/>
+    </issue>
+
+    <issue
+        id="ComposableNaming"
+        message="Composable functions with a return type should start with a lowercase letter"
+        errorLine1="    fun Clickable("
+        errorLine2="        ~~~~~~~~~">
+        <location
+            file="src/commonMain/kotlin/androidx/compose/material/Text.kt"/>
+    </issue>
+
+    <issue
         id="PrimitiveInCollection"
         message="variable crossAxisSizes with type List&lt;Integer>: replace with IntList"
         errorLine1="        val crossAxisSizes = mutableListOf&lt;Int>()"
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextSamples.kt
new file mode 100644
index 0000000..7d5c9ee
--- /dev/null
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextSamples.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material.Text
+import androidx.compose.material.TextDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withLink
+
+@Composable
+@Sampled
+fun AnnotatedStringWithLinks() {
+    Text(buildAnnotatedString {
+        append("Build better apps faster with ")
+        withLink(TextDefaults.Url(url = "https://developer.android.com/jetpack/compose")) {
+            append("Jetpack Compose")
+        }
+    })
+}
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
index ec962fe..c0eda44 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -43,7 +42,6 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTestApi::class)
 class BadgeScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
index fe4e434..893853e 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -48,7 +47,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTestApi::class)
+@OptIn(ExperimentalMaterialApi::class)
 class BottomNavigationScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ChipScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ChipScreenshotTest.kt
index d646868b..fdc00a0 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ChipScreenshotTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ChipScreenshotTest.kt
@@ -24,7 +24,6 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -39,7 +38,7 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTestApi::class)
+@OptIn(ExperimentalMaterialApi::class)
 class ChipScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DrawerTest.kt
index 502276f..9a4b666 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -36,7 +36,6 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertHasClickAction
@@ -1062,7 +1061,6 @@
             assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
         }
 
-        @OptIn(ExperimentalTestApi::class)
         rule.onNodeWithTag(contentTag)
             .performTouchInput { swipeUp(endY = peekHeight) }
 
@@ -1083,7 +1081,6 @@
             assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Expanded)
         }
 
-        @OptIn(ExperimentalTestApi::class)
         rule.onNodeWithTag(bottomDrawerTag)
             .performTouchInput { swipeDown(endY = peekHeight) }
 
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt
index 96c7496..a503594 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/NavigationRailScreenshotTest.kt
@@ -33,7 +33,6 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -50,7 +49,6 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTestApi::class)
 class NavigationRailScreenshotTest {
     @get:Rule
     val composeTestRule = createComposeRule()
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ObservableThemeTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
index 758f3f1..c718ecb 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
@@ -33,7 +33,6 @@
 import androidx.compose.testutils.doFramesUntilNoChangesPending
 import androidx.compose.testutils.forGivenTestCase
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -49,7 +48,6 @@
  */
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTestApi::class)
 class ObservableThemeTest {
     @get:Rule
     val composeTestRule = createAndroidComposeRule<ComponentActivity>()
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
index 236bc09..dba7db3 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
@@ -30,7 +30,6 @@
 import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.ProgressBarRangeInfo
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertRangeInfoEquals
@@ -51,7 +50,6 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTestApi::class)
 class ProgressIndicatorTest {
 
     private val ExpectedLinearWidth = 240.dp
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TabScreenshotTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
index 61a1821..a0b17e9 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -48,7 +47,6 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTestApi::class)
 class TabScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
index e32bd12..9b8e5a3 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
@@ -16,8 +16,10 @@
 
 package androidx.compose.material
 
+import android.os.Build
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.testutils.assertContainsColor
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
@@ -26,12 +28,15 @@
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.hasClickAction
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performSemanticsAction
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.TextUnit
@@ -39,6 +44,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -279,4 +285,41 @@
             color == expectedColor
         })
     }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun fromHtml_links_getColorFromMaterialTheme() {
+        var primary: Color? = null
+        rule.setMaterialContent {
+            primary = MaterialTheme.colors.primary
+            Text(TextDefaults.fromHtml("<a href=url>link</a>"))
+        }
+
+        rule.runOnIdle {
+            assertThat(primary).isNotNull()
+        }
+        rule.onNode(hasClickAction(), useUnmergedTree = true)
+            .captureToImage()
+            .assertContainsColor(primary!!)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun links_getColorFromMaterialTheme() {
+        var primary: Color? = null
+        rule.setMaterialContent {
+            primary = MaterialTheme.colors.primary
+            Text(buildAnnotatedString {
+                append("link")
+                addLink(TextDefaults.Url(url = "url"), 0, 4)
+            })
+        }
+
+        rule.runOnIdle {
+            assertThat(primary).isNotNull()
+        }
+        rule.onNode(hasClickAction(), useUnmergedTree = true)
+            .captureToImage()
+            .assertContainsColor(primary!!)
+    }
 }
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
index 3ebc53d..87ac6cd 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
 import androidx.compose.material.AnchoredDraggableState
 import androidx.compose.material.DraggableAnchors
 import androidx.compose.material.ExperimentalMaterialApi
@@ -1077,6 +1078,21 @@
             )
     }
 
+    // Regression test for b/332930104
+    @Test
+    fun draggableAnchors_equals() {
+        val state = AnchoredDraggableState(
+            initialValue = B,
+            positionalThreshold = defaultPositionalThreshold,
+            velocityThreshold = defaultVelocityThreshold,
+            animationSpec = defaultAnimationSpec
+        )
+        val draggableAnchors = Modifier.draggableAnchors(state, Orientation.Vertical) { _, _ ->
+            DraggableAnchors<AnchoredDraggableTestValue> { } to A
+        }
+        assertThat(draggableAnchors).isNotEqualTo(Modifier)
+    }
+
     private suspend fun suspendIndefinitely() = suspendCancellableCoroutine<Unit> { }
 
     private class HandPumpTestFrameClock : MonotonicFrameClock {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
index de77488..d90d9b3 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
@@ -835,7 +835,7 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
 
-        other as DraggableAnchorsElement<*>
+        if (other !is DraggableAnchorsElement<*>) return false
 
         if (state != other.state) return false
         if (anchors != other.anchors) return false
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
index 5fc4f0d..05d4453 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
@@ -20,18 +20,23 @@
 import androidx.compose.foundation.text.InlineTextContent
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.structuralEqualityPolicy
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.isSpecified
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.LinkInteractionListener
 import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.fromHtml
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.text.style.TextOverflow
@@ -395,3 +400,76 @@
     val mergedStyle = LocalTextStyle.current.merge(value)
     CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
 }
+
+/** Contains the methods to be used by [Text] */
+object TextDefaults {
+    /**
+     * Converts a string with HTML tags into [AnnotatedString]. Applies default styling from the
+     * [MaterialTheme] to links present in the [htmlString].
+     *
+     * Check [androidx.compose.ui.text.AnnotatedString.Companion.fromHtml] for more details on
+     * supported tags and usage.
+     *
+     * @param htmlString HTML-tagged string to be parsed to construct AnnotatedString
+     * @param linkStyle style to be applied to links present in the string
+     * @param linkFocusedStyle style to be applied to links present in the string when they are
+     * focused
+     * @param linkHoveredStyle style to be applied to links present in the string when they are
+     * hovered
+     * @param linkInteractionListener a listener that will be attached to links that are present in
+     * the string and triggered when user clicks on those links. When set to null, which is
+     * a default, the system will try to open the corresponding links with the
+     * [androidx.compose.ui.platform.UriHandler] composition local
+     *
+     * @see androidx.compose.ui.text.AnnotatedString.Companion.fromHtml
+     */
+    @Composable
+    @ReadOnlyComposable
+    fun fromHtml(
+        htmlString: String,
+        linkStyle: SpanStyle? = SpanStyle(color = MaterialTheme.colors.primary),
+        linkFocusedStyle: SpanStyle? = null,
+        linkHoveredStyle: SpanStyle? = null,
+        linkInteractionListener: LinkInteractionListener? = null
+    ): AnnotatedString {
+        return AnnotatedString.fromHtml(
+            htmlString, linkStyle, linkFocusedStyle, linkHoveredStyle, linkInteractionListener
+        )
+    }
+
+    /**
+     * Constructs a [LinkAnnotation.Url] and applies default styling from the [MaterialTheme]
+     *
+     * @sample androidx.compose.material.samples.AnnotatedStringWithLinks
+     */
+    @Composable
+    @ReadOnlyComposable
+    fun Url(
+        url: String,
+        linkStyle: SpanStyle? = SpanStyle(color = MaterialTheme.colors.primary),
+        linkFocusedStyle: SpanStyle? = null,
+        linkHoveredStyle: SpanStyle? = null,
+        linkInteractionListener: LinkInteractionListener? = null
+    ): LinkAnnotation.Url {
+        return LinkAnnotation.Url(
+            url, linkStyle, linkFocusedStyle, linkHoveredStyle, linkInteractionListener
+        )
+    }
+
+    /**
+     * Constructs a [LinkAnnotation.Clickable] and applies default styling from the [MaterialTheme]
+     */
+    @Composable
+    @ReadOnlyComposable
+    fun Clickable(
+        tag: String,
+        linkStyle: SpanStyle? = SpanStyle(color = MaterialTheme.colors.primary),
+        linkFocusedStyle: SpanStyle? = null,
+        linkHoveredStyle: SpanStyle? = null,
+        linkInteractionListener: LinkInteractionListener?
+    ): LinkAnnotation.Clickable {
+        return LinkAnnotation.Clickable(
+            tag, linkStyle, linkFocusedStyle, linkHoveredStyle, linkInteractionListener
+        )
+    }
+}
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index 6adbaa6..9f5beea 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.compose.material3.adaptive.layout {
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface AdaptStrategy {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface AdaptStrategy {
     method public String adapt();
     field public static final androidx.compose.material3.adaptive.layout.AdaptStrategy.Companion Companion;
   }
@@ -11,6 +11,9 @@
     property public final androidx.compose.material3.adaptive.layout.AdaptStrategy Hide;
   }
 
+  public sealed interface AnimatedPaneScope extends androidx.compose.animation.AnimatedVisibilityScope {
+  }
+
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class HingePolicy {
     field public static final androidx.compose.material3.adaptive.layout.HingePolicy.Companion Companion;
   }
@@ -56,6 +59,10 @@
     property public final String Hidden;
   }
 
+  public final class PaneKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void AnimatedPane(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
+  }
+
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
     ctor public PaneScaffoldDirective(int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, float defaultPanePreferredWidth, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
     method public float getDefaultPanePreferredWidth();
@@ -77,7 +84,7 @@
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneScaffoldScope {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldScope {
     method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
   }
 
@@ -113,17 +120,13 @@
     property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane;
   }
 
-  public final class ThreePaneScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void AnimatedPane(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> content);
-  }
-
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum ThreePaneScaffoldRole {
     enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Primary;
     enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Secondary;
     enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Tertiary;
   }
 
-  public interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope {
     method public androidx.compose.animation.EnterTransition getEnterTransition();
     method public androidx.compose.animation.ExitTransition getExitTransition();
     method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getPositionAnimationSpec();
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index 6adbaa6..9f5beea 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.compose.material3.adaptive.layout {
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface AdaptStrategy {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public sealed interface AdaptStrategy {
     method public String adapt();
     field public static final androidx.compose.material3.adaptive.layout.AdaptStrategy.Companion Companion;
   }
@@ -11,6 +11,9 @@
     property public final androidx.compose.material3.adaptive.layout.AdaptStrategy Hide;
   }
 
+  public sealed interface AnimatedPaneScope extends androidx.compose.animation.AnimatedVisibilityScope {
+  }
+
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class HingePolicy {
     field public static final androidx.compose.material3.adaptive.layout.HingePolicy.Companion Companion;
   }
@@ -56,6 +59,10 @@
     property public final String Hidden;
   }
 
+  public final class PaneKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void AnimatedPane(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
+  }
+
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
     ctor public PaneScaffoldDirective(int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, float defaultPanePreferredWidth, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
     method public float getDefaultPanePreferredWidth();
@@ -77,7 +84,7 @@
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneScaffoldScope {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldScope {
     method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
   }
 
@@ -113,17 +120,13 @@
     property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane;
   }
 
-  public final class ThreePaneScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void AnimatedPane(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> content);
-  }
-
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum ThreePaneScaffoldRole {
     enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Primary;
     enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Secondary;
     enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Tertiary;
   }
 
-  public interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope {
     method public androidx.compose.animation.EnterTransition getEnterTransition();
     method public androidx.compose.animation.ExitTransition getExitTransition();
     method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getPositionAnimationSpec();
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
index 6f73836..5ae0da6 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
@@ -17,25 +17,43 @@
 package androidx.compose.material3.adaptive.layout
 
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
 
 /**
  * Provides the information about how the associated pane should be adapted if it cannot be
  * displayed in its [PaneAdaptedValue.Expanded] state.
  */
 @ExperimentalMaterial3AdaptiveApi
-interface AdaptStrategy {
+@Stable
+sealed interface AdaptStrategy {
     /**
      * Override this function to provide the resulted adapted state.
      */
     fun adapt(): PaneAdaptedValue
 
-    private class BaseAdaptStrategy(
+    @Immutable
+    private class SimpleAdaptStrategy(
         private val description: String,
-        private val adaptedState: PaneAdaptedValue
+        private val adaptedValue: PaneAdaptedValue
     ) : AdaptStrategy {
-        override fun adapt() = adaptedState
+        override fun adapt() = adaptedValue
 
         override fun toString() = "AdaptStrategy[$description]"
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is SimpleAdaptStrategy) return false
+            if (description != other.description) return false
+            if (adaptedValue != other.adaptedValue) return false
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = description.hashCode()
+            result = 31 * result + adaptedValue.hashCode()
+            return result
+        }
     }
 
     companion object {
@@ -43,6 +61,6 @@
          * The default [AdaptStrategy] that suggests the layout to hide the associated pane when
          * it has to be adapted, i.e., cannot be displayed in its [PaneAdaptedValue.Expanded] state.
          */
-        val Hide: AdaptStrategy = BaseAdaptStrategy("Hide", PaneAdaptedValue.Hidden)
+        val Hide: AdaptStrategy = SimpleAdaptStrategy("Hide", PaneAdaptedValue.Hidden)
     }
 }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
new file mode 100644
index 0000000..62cf8a1
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+
+/**
+ * The root composable of pane contents in a [ThreePaneScaffold] that supports default motions
+ * during pane switching. It's recommended to use this composable to wrap your own contents when
+ * passing them into pane parameters of the scaffold functions, therefore your panes can have a
+ * nice default animation for free.
+ *
+ * @param modifier The modifier applied to the [AnimatedPane].
+ * @param content The content of the [AnimatedPane]. Also see [AnimatedPaneScope].
+ *
+ * See usage samples at:
+ * @sample androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSample
+ * @sample androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPane
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO: address before moving to beta
+@OptIn(ExperimentalAnimationApi::class)
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+fun ThreePaneScaffoldScope.AnimatedPane(
+    modifier: Modifier = Modifier,
+    content: (@Composable AnimatedPaneScope.() -> Unit),
+) {
+    val keepShowing = scaffoldStateTransition.currentState[role] != PaneAdaptedValue.Hidden &&
+        scaffoldStateTransition.targetState[role] != PaneAdaptedValue.Hidden
+    scaffoldStateTransition.AnimatedVisibility(
+        visible = { value: ThreePaneScaffoldValue -> value[role] != PaneAdaptedValue.Hidden },
+        modifier = modifier
+            .animatedPane()
+            .animateBounds(
+                animateFraction = scaffoldStateTransitionFraction,
+                positionAnimationSpec = positionAnimationSpec,
+                sizeAnimationSpec = sizeAnimationSpec,
+                lookaheadScope = this,
+                enabled = keepShowing
+            )
+            .then(if (keepShowing) Modifier else Modifier.clipToBounds()),
+        enter = enterTransition,
+        exit = exitTransition
+    ) {
+        AnimatedPaneScopeImpl(this).content()
+    }
+}
+
+/**
+ * Scope for the content of [AnimatedPane]. It extends from the necessary animation scopes so
+ * developers can use the info carried by the scopes to do certain customizations.
+ */
+sealed interface AnimatedPaneScope : AnimatedVisibilityScope
+
+private class AnimatedPaneScopeImpl(
+    animatedVisibilityScope: AnimatedVisibilityScope
+) : AnimatedPaneScope, AnimatedVisibilityScope by animatedVisibilityScope
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index 3d94377..e164e2f 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -30,7 +30,7 @@
  * Scope for the panes of pane scaffolds.
  */
 @ExperimentalMaterial3AdaptiveApi
-interface PaneScaffoldScope {
+sealed interface PaneScaffoldScope {
     /**
      * This modifier specifies the preferred width for a pane, and the pane scaffold implementation
      * will try its best to respect this width when the associated pane is rendered as a fixed pane,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index bdce358..667a60c 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -16,10 +16,8 @@
 
 package androidx.compose.material3.adaptive.layout
 
-import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.EnterTransition
 import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.animation.core.FiniteAnimationSpec
 import androidx.compose.animation.core.SeekableTransitionState
 import androidx.compose.animation.core.Transition
@@ -36,7 +34,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.Measurable
@@ -688,45 +685,6 @@
     }
 }
 
-/**
- * The root composable of pane contents in a [ThreePaneScaffold] that supports default motions
- * during pane switching. It's recommended to use this composable to wrap your own contents when
- * passing them into pane parameters of the scaffold functions, therefore your panes can have a
- * nice default animation for free.
- *
- * See usage samples at:
- * @sample androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSample
- * @sample androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPane
- */
-@Suppress("IllegalExperimentalApiUsage") // TODO: address before moving to beta
-@OptIn(ExperimentalAnimationApi::class)
-@ExperimentalMaterial3AdaptiveApi
-@Composable
-fun ThreePaneScaffoldScope.AnimatedPane(
-    modifier: Modifier = Modifier,
-    content: (@Composable ThreePaneScaffoldScope.() -> Unit),
-) {
-    val keepShowing = scaffoldStateTransition.currentState[role] != PaneAdaptedValue.Hidden &&
-        scaffoldStateTransition.targetState[role] != PaneAdaptedValue.Hidden
-    scaffoldStateTransition.AnimatedVisibility(
-        visible = { value: ThreePaneScaffoldValue -> value[role] != PaneAdaptedValue.Hidden },
-        modifier = modifier
-            .animatedPane()
-            .animateBounds(
-                animateFraction = scaffoldStateTransitionFraction,
-                positionAnimationSpec = positionAnimationSpec,
-                sizeAnimationSpec = sizeAnimationSpec,
-                lookaheadScope = this,
-                enabled = keepShowing
-            )
-            .graphicsLayer(clip = !keepShowing),
-        enter = enterTransition,
-        exit = exitTransition
-    ) {
-        content()
-    }
-}
-
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private class PaneMeasurable(
     val measurable: Measurable,
@@ -778,8 +736,8 @@
 /**
  * Scope for the panes of [ThreePaneScaffold].
  */
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-interface ThreePaneScaffoldScope : PaneScaffoldScope, LookaheadScope {
+@ExperimentalMaterial3AdaptiveApi
+sealed interface ThreePaneScaffoldScope : PaneScaffoldScope, LookaheadScope {
     /**
      * The [ThreePaneScaffoldRole] of the current pane in the scope.
      */
@@ -793,6 +751,7 @@
     /**
      * The current fraction of the scaffold state transition.
      */
+    // TODO(b/333403487): change this to lambda to defer value reading
     val scaffoldStateTransitionFraction: Float
 
     /**
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/current.txt b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
index 71aa3e9..1cd6e10 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
@@ -45,7 +45,7 @@
     method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public sealed interface NavigationSuiteScope {
     method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
index 71aa3e9..1cd6e10 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
@@ -45,7 +45,7 @@
     method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public sealed interface NavigationSuiteScope {
     method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
index 4497779..93dbf86 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
@@ -286,7 +286,7 @@
 
 /** The scope associated with the [NavigationSuiteScope]. */
 @ExperimentalMaterial3AdaptiveNavigationSuiteApi
-interface NavigationSuiteScope {
+sealed interface NavigationSuiteScope {
 
     /**
      * This function sets the parameters of the default Material navigation item to be used with the
diff --git a/compose/material3/material3/api/api_lint.ignore b/compose/material3/material3/api/api_lint.ignore
index af683b8..3ec7be4 100644
--- a/compose/material3/material3/api/api_lint.ignore
+++ b/compose/material3/material3/api/api_lint.ignore
@@ -3,6 +3,14 @@
     Must avoid boxed primitives (`java.lang.Long`)
 
 
+ExecutorRegistration: androidx.compose.material3.TextDefaults#Clickable(String, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.LinkInteractionListener):
+    Registration methods should have overload that accepts delivery Executor: `Clickable`
+ExecutorRegistration: androidx.compose.material3.TextDefaults#Url(String, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.LinkInteractionListener):
+    Registration methods should have overload that accepts delivery Executor: `Url`
+ExecutorRegistration: androidx.compose.material3.TextDefaults#fromHtml(String, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.SpanStyle, androidx.compose.ui.text.LinkInteractionListener):
+    Registration methods should have overload that accepts delivery Executor: `fromHtml`
+
+
 GetterSetterNames: androidx.compose.material3.SheetState#getHasExpandedState():
     Getter for boolean property `hasExpandedState` is named `getHasExpandedState` but should match the property name. Use `@get:JvmName` to rename.
 GetterSetterNames: androidx.compose.material3.SheetState#getHasPartiallyExpandedState():
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 8f50dbc..40b8512 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1783,6 +1783,13 @@
     method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.material3.TabPosition>,kotlin.Unit> indicator, optional kotlin.jvm.functions.Function0<kotlin.Unit> divider, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
   }
 
+  public final class TextDefaults {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.LinkAnnotation.Clickable Clickable(String tag, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.LinkAnnotation.Url Url(String url, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, optional androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.AnnotatedString fromHtml(String htmlString, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, optional androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    field public static final androidx.compose.material3.TextDefaults INSTANCE;
+  }
+
   @androidx.compose.runtime.Immutable public final class TextFieldColors {
     ctor public TextFieldColors(long focusedTextColor, long unfocusedTextColor, long disabledTextColor, long errorTextColor, long focusedContainerColor, long unfocusedContainerColor, long disabledContainerColor, long errorContainerColor, long cursorColor, long errorCursorColor, androidx.compose.foundation.text.selection.TextSelectionColors textSelectionColors, long focusedIndicatorColor, long unfocusedIndicatorColor, long disabledIndicatorColor, long errorIndicatorColor, long focusedLeadingIconColor, long unfocusedLeadingIconColor, long disabledLeadingIconColor, long errorLeadingIconColor, long focusedTrailingIconColor, long unfocusedTrailingIconColor, long disabledTrailingIconColor, long errorTrailingIconColor, long focusedLabelColor, long unfocusedLabelColor, long disabledLabelColor, long errorLabelColor, long focusedPlaceholderColor, long unfocusedPlaceholderColor, long disabledPlaceholderColor, long errorPlaceholderColor, long focusedSupportingTextColor, long unfocusedSupportingTextColor, long disabledSupportingTextColor, long errorSupportingTextColor, long focusedPrefixColor, long unfocusedPrefixColor, long disabledPrefixColor, long errorPrefixColor, long focusedSuffixColor, long unfocusedSuffixColor, long disabledSuffixColor, long errorSuffixColor);
     method public androidx.compose.material3.TextFieldColors copy(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long focusedContainerColor, optional long unfocusedContainerColor, optional long disabledContainerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors? textSelectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
@@ -2154,9 +2161,11 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemInfo {
+    method public androidx.compose.ui.geometry.Rect getMaskRect();
     method public float getMaxSize();
     method public float getMinSize();
     method public float getSize();
+    property public abstract androidx.compose.ui.geometry.Rect maskRect;
     property public abstract float maxSize;
     property public abstract float minSize;
     property public abstract float size;
@@ -2190,9 +2199,7 @@
   }
 
   public final class CarouselStateKt {
-    method public static float endOffset(androidx.compose.material3.carousel.CarouselItemInfo);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.carousel.CarouselState rememberCarouselState(optional int initialItem, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
-    method public static float startOffset(androidx.compose.material3.carousel.CarouselItemInfo);
   }
 
 }
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 8f50dbc..40b8512 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1783,6 +1783,13 @@
     method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.material3.TabPosition>,kotlin.Unit> indicator, optional kotlin.jvm.functions.Function0<kotlin.Unit> divider, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
   }
 
+  public final class TextDefaults {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.LinkAnnotation.Clickable Clickable(String tag, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.LinkAnnotation.Url Url(String url, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, optional androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.text.AnnotatedString fromHtml(String htmlString, optional androidx.compose.ui.text.SpanStyle? linkStyle, optional androidx.compose.ui.text.SpanStyle? linkFocusedStyle, optional androidx.compose.ui.text.SpanStyle? linkHoveredStyle, optional androidx.compose.ui.text.LinkInteractionListener? linkInteractionListener);
+    field public static final androidx.compose.material3.TextDefaults INSTANCE;
+  }
+
   @androidx.compose.runtime.Immutable public final class TextFieldColors {
     ctor public TextFieldColors(long focusedTextColor, long unfocusedTextColor, long disabledTextColor, long errorTextColor, long focusedContainerColor, long unfocusedContainerColor, long disabledContainerColor, long errorContainerColor, long cursorColor, long errorCursorColor, androidx.compose.foundation.text.selection.TextSelectionColors textSelectionColors, long focusedIndicatorColor, long unfocusedIndicatorColor, long disabledIndicatorColor, long errorIndicatorColor, long focusedLeadingIconColor, long unfocusedLeadingIconColor, long disabledLeadingIconColor, long errorLeadingIconColor, long focusedTrailingIconColor, long unfocusedTrailingIconColor, long disabledTrailingIconColor, long errorTrailingIconColor, long focusedLabelColor, long unfocusedLabelColor, long disabledLabelColor, long errorLabelColor, long focusedPlaceholderColor, long unfocusedPlaceholderColor, long disabledPlaceholderColor, long errorPlaceholderColor, long focusedSupportingTextColor, long unfocusedSupportingTextColor, long disabledSupportingTextColor, long errorSupportingTextColor, long focusedPrefixColor, long unfocusedPrefixColor, long disabledPrefixColor, long errorPrefixColor, long focusedSuffixColor, long unfocusedSuffixColor, long disabledSuffixColor, long errorSuffixColor);
     method public androidx.compose.material3.TextFieldColors copy(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long focusedContainerColor, optional long unfocusedContainerColor, optional long disabledContainerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors? textSelectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
@@ -2154,9 +2161,11 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemInfo {
+    method public androidx.compose.ui.geometry.Rect getMaskRect();
     method public float getMaxSize();
     method public float getMinSize();
     method public float getSize();
+    property public abstract androidx.compose.ui.geometry.Rect maskRect;
     property public abstract float maxSize;
     property public abstract float minSize;
     property public abstract float size;
@@ -2190,9 +2199,7 @@
   }
 
   public final class CarouselStateKt {
-    method public static float endOffset(androidx.compose.material3.carousel.CarouselItemInfo);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.carousel.CarouselState rememberCarouselState(optional int initialItem, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
-    method public static float startOffset(androidx.compose.material3.carousel.CarouselItemInfo);
   }
 
 }
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 242201a..a33f5f7 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -89,12 +89,6 @@
 import androidx.compose.material3.samples.InputChipWithAvatarSample
 import androidx.compose.material3.samples.LargeFloatingActionButtonSample
 import androidx.compose.material3.samples.LeadingIconTabs
-import androidx.compose.material3.samples.LegacyCircularProgressIndicatorSample
-import androidx.compose.material3.samples.LegacyIndeterminateCircularProgressIndicatorSample
-import androidx.compose.material3.samples.LegacyIndeterminateLinearProgressIndicatorSample
-import androidx.compose.material3.samples.LegacyLinearProgressIndicatorSample
-import androidx.compose.material3.samples.LegacyRangeSliderSample
-import androidx.compose.material3.samples.LegacySliderSample
 import androidx.compose.material3.samples.LinearProgressIndicatorSample
 import androidx.compose.material3.samples.MenuSample
 import androidx.compose.material3.samples.MenuWithScrollStateSample
@@ -847,34 +841,6 @@
         sourceUrl = ProgressIndicatorsExampleSourceUrl
     ) {
         IndeterminateCircularProgressIndicatorSample()
-    },
-    Example(
-        name = ::LegacyLinearProgressIndicatorSample.name,
-        description = ProgressIndicatorsExampleDescription,
-        sourceUrl = ProgressIndicatorsExampleSourceUrl
-    ) {
-        LegacyLinearProgressIndicatorSample()
-    },
-    Example(
-        name = ::LegacyIndeterminateLinearProgressIndicatorSample.name,
-        description = ProgressIndicatorsExampleDescription,
-        sourceUrl = ProgressIndicatorsExampleSourceUrl
-    ) {
-        LegacyIndeterminateLinearProgressIndicatorSample()
-    },
-    Example(
-        name = ::LegacyCircularProgressIndicatorSample.name,
-        description = ProgressIndicatorsExampleDescription,
-        sourceUrl = ProgressIndicatorsExampleSourceUrl
-    ) {
-        LegacyCircularProgressIndicatorSample()
-    },
-    Example(
-        name = ::LegacyIndeterminateCircularProgressIndicatorSample.name,
-        description = ProgressIndicatorsExampleDescription,
-        sourceUrl = ProgressIndicatorsExampleSourceUrl
-    ) {
-        LegacyIndeterminateCircularProgressIndicatorSample()
     }
 )
 
@@ -965,13 +931,6 @@
 private const val SlidersExampleSourceUrl = "$SampleSourceUrl/SliderSamples.kt"
 val SlidersExamples = listOf(
     Example(
-        name = ::LegacySliderSample.name,
-        description = SlidersExampleDescription,
-        sourceUrl = SlidersExampleSourceUrl
-    ) {
-        LegacySliderSample()
-    },
-    Example(
         name = ::SliderSample.name,
         description = SlidersExampleDescription,
         sourceUrl = SlidersExampleSourceUrl
@@ -1000,13 +959,6 @@
         SliderWithCustomTrackAndThumb()
     },
     Example(
-        name = ::LegacyRangeSliderSample.name,
-        description = SlidersExampleDescription,
-        sourceUrl = SlidersExampleSourceUrl
-    ) {
-        LegacyRangeSliderSample()
-    },
-    Example(
         name = ::RangeSliderSample.name,
         description = SlidersExampleDescription,
         sourceUrl = SlidersExampleSourceUrl
diff --git a/compose/material3/material3/lint-baseline.xml b/compose/material3/material3/lint-baseline.xml
index 4395fd6..c30eb05 100644
--- a/compose/material3/material3/lint-baseline.xml
+++ b/compose/material3/material3/lint-baseline.xml
@@ -155,6 +155,24 @@
     </issue>
 
     <issue
+        id="ComposableNaming"
+        message="Composable functions with a return type should start with a lowercase letter"
+        errorLine1="    fun Url("
+        errorLine2="        ~~~">
+        <location
+            file="src/commonMain/kotlin/androidx/compose/material3/Text.kt"/>
+    </issue>
+
+    <issue
+        id="ComposableNaming"
+        message="Composable functions with a return type should start with a lowercase letter"
+        errorLine1="    fun Clickable("
+        errorLine2="        ~~~~~~~~~">
+        <location
+            file="src/commonMain/kotlin/androidx/compose/material3/Text.kt"/>
+    </issue>
+
+    <issue
         id="UnsafeOptInUsageError"
         message="This declaration is opt-in and its usage should be marked with `@androidx.compose.material3.ExperimentalMaterial3Api` or `@OptIn(markerClass = androidx.compose.material3.ExperimentalMaterial3Api.class)`">
         <location
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
index 90522ff..cf25fa1 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
@@ -31,7 +31,6 @@
 import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
 import androidx.compose.material3.carousel.HorizontalUncontainedCarousel
 import androidx.compose.material3.carousel.rememberCarouselState
-import androidx.compose.material3.carousel.startOffset
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.graphicsLayer
@@ -72,8 +71,7 @@
     ) { i ->
         val item = items[i]
         Card(
-            modifier = Modifier
-                .height(205.dp)
+            modifier = Modifier.height(205.dp)
         ) {
             Image(
                 painter = painterResource(id = item.imageResId),
@@ -177,7 +175,7 @@
                 Text(
                     text = "sample text",
                     modifier = Modifier.graphicsLayer {
-                        translationX = carouselItemInfo.startOffset()
+                        translationX = carouselItemInfo.maskRect.left
                     }
                 )
             }
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextSamples.kt
new file mode 100644
index 0000000..11f84f0
--- /dev/null
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextSamples.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withLink
+
+@Composable
+@Sampled
+fun AnnotatedStringWithLinks() {
+    Text(buildAnnotatedString {
+        append("Build better apps faster with ")
+        withLink(TextDefaults.Url(url = "https://developer.android.com/jetpack/compose")) {
+            append("Jetpack Compose")
+        }
+    })
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
index c08194bc..a04d59f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonScreenshotTest.kt
@@ -26,7 +26,6 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.hasClickAction
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -44,7 +43,6 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTestApi::class)
 class ButtonScreenshotTest {
 
     @get:Rule
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt
index 431e03d..97ceec8 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/NavigationDrawerItemScreenshotTest.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -48,7 +47,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalTestApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
 class NavigationDrawerItemScreenshotTest {
     @get:Rule
     val composeTestRule = createComposeRule()
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
index 893ac64..6cf6d36 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -36,7 +35,6 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTestApi::class)
 class SnackbarScreenshotTest {
 
     @get:Rule
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextTest.kt
index 7000381..1e877c4 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextTest.kt
@@ -15,11 +15,13 @@
  */
 
 package androidx.compose.material3
+import android.os.Build
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertContainsColor
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
@@ -27,6 +29,8 @@
 import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.hasClickAction
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
@@ -34,6 +38,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.TextUnit
@@ -41,6 +46,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -350,4 +356,31 @@
             color == expectedColor
         })
     }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun fromHtml_links_getColorFromMaterialTheme() {
+        rule.setMaterialContent(lightColorScheme(primary = Color.Red)) {
+            Text(TextDefaults.fromHtml("<a href=url>link</a>"))
+        }
+
+        rule.onNode(hasClickAction(), useUnmergedTree = true)
+            .captureToImage()
+            .assertContainsColor(Color.Red)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun links_getColorFromMaterialTheme() {
+        rule.setMaterialContent(lightColorScheme(primary = Color.Red)) {
+            Text(buildAnnotatedString {
+                append("link")
+                addLink(TextDefaults.Url(url = "url"), 0, 4)
+            })
+        }
+
+        rule.onNode(hasClickAction(), useUnmergedTree = true)
+            .captureToImage()
+            .assertContainsColor(Color.Red)
+    }
 }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
index 4f324e81..f6a7ffd 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
@@ -75,6 +75,7 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.dx.mockito.inline.extended.MockedMethod
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.PI
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -521,6 +522,32 @@
 
     @Test
     @OptIn(ExperimentalTestApi::class)
+    fun timeInput_writeMinute_updatesCurrentAngle() {
+        val state = TimePickerState(initialHour = 10, initialMinute = 0, is24Hour = true)
+
+        rule.setMaterialContent(lightColorScheme()) {
+            TimeInput(state)
+        }
+
+        rule.onNodeWithText("10")
+            .performKeyInput {
+                pressKey(Key.One)
+                pressKey(Key.Two)
+            }
+
+        rule.onNodeWithText("00")
+            .performKeyInput {
+                pressKey(Key.Four)
+                pressKey(Key.Five)
+            }
+
+        assertThat(state.currentAngle.value)
+            .isWithin(0.0001f)
+            .of(PI.toFloat())
+    }
+
+    @Test
+    @OptIn(ExperimentalTestApi::class)
     fun timeInput_24Hour_writeMidnight() {
         val state = TimePickerState(initialHour = 10, initialMinute = 23, is24Hour = true)
 
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
index 95b8a25..5d529fa 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
@@ -180,50 +180,19 @@
     }
 
     @Test
-    fun carousel_calculateBeyondViewportPageCount() {
-        val xSmallSize = 5f
-        val smallSize = 100f
-        val mediumSize = 200f
-        val largeSize = 400f
-        val keylineList = keylineListOf(carouselMainAxisSize = 1000f, 0f, CarouselAlignment.Start) {
-            add(xSmallSize, isAnchor = true)
-            add(largeSize)
-            add(mediumSize)
-            add(mediumSize)
-            add(smallSize)
-            add(smallSize)
-            add(xSmallSize, isAnchor = true)
-        }
-        val strategy = Strategy { _, _ -> keylineList }.apply(
-            availableSpace = 1000f,
-            itemSpacing = 0f,
-            beforeContentPadding = 0f,
-            afterContentPadding = 0f
-        )
-        val outOfBoundsNum = calculateBeyondViewportPageCount(strategy)
-        // With this strategy, we expect 3 loaded items
-        val loadedItems = 3
-
-        assertThat(outOfBoundsNum).isEqualTo(
-            (keylineList.filter { !it.isAnchor }.size - loadedItems) + 1
-        )
-    }
-
-    @Test
     fun carousel_correctlyCalculatesMaxScrollOffsetWithItemSpacing() {
         rule.setMaterialContent(lightColorScheme()) {
             val state = rememberCarouselState { 10 }.also {
                 carouselState = it
             }
-            val strategy = Strategy { availableSpace, itemSpacing ->
-                keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Start) {
-                    add(10f, isAnchor = true)
-                    add(186f)
-                    add(122f)
-                    add(56f)
-                    add(10f, isAnchor = true)
-                }
-            }.apply(
+            val strategy = Strategy(
+                defaultKeylines = keylineListOf(380f, 0f, CarouselAlignment.Start) {
+                        add(10f, isAnchor = true)
+                        add(186f)
+                        add(122f)
+                        add(56f)
+                        add(10f, isAnchor = true)
+                },
                 availableSpace = 380f,
                 itemSpacing = 8f,
                 beforeContentPadding = 0f,
@@ -285,6 +254,7 @@
                     )
                 },
                 flingBehavior = flingBehavior(state),
+                maxNonFocalVisibleItemCount = 2,
                 modifier = modifier.testTag(CarouselTestTag),
                 itemSpacing = 0.dp,
                 contentPadding = PaddingValues(0.dp),
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
index a6b9613..0410575 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
@@ -27,25 +27,34 @@
     @Test
     fun testSnapPosition_forCenterAlignedStrategy() {
         val itemCount = 6
-        val map = calculateSnapPositions(testCenterAlignedStrategy(), itemCount)
+        val strategy = testCenterAlignedStrategy()
         val expectedSnapPositions = arrayListOf(0, 200, 300, 300, 400, 600)
-        repeat(itemCount) { i -> assertThat(map[i]).isEqualTo(expectedSnapPositions[i]) }
+        repeat(itemCount) { i ->
+            assertThat(getSnapPositionOffset(strategy, i, itemCount))
+                .isEqualTo(expectedSnapPositions[i])
+        }
     }
 
     @Test
     fun testSnapPosition_forStartAlignedStrategy() {
         val itemCount = 6
-        val map = calculateSnapPositions(testStartAlignedStrategy(), itemCount)
+        val strategy = testStartAlignedStrategy()
         val expectedSnapPositions = arrayListOf(0, 0, 100, 200, 400, 600)
-        repeat(itemCount) { i -> assertThat(map[i]).isEqualTo(expectedSnapPositions[i]) }
+        repeat(itemCount) { i ->
+            assertThat(getSnapPositionOffset(strategy, i, itemCount))
+                .isEqualTo(expectedSnapPositions[i])
+        }
     }
 
     @Test
     fun testSnapPosition_forStartAlignedStrategyWithMultipleFocal() {
         val itemCount = 5
-        val map = calculateSnapPositions(testStartAlignedStrategyWithMultipleFocal(), itemCount)
+        val strategy = testStartAlignedStrategyWithMultipleFocal()
         val expectedSnapPositions = arrayListOf(0, 0, 75, 200, 200)
-        repeat(itemCount) { i -> assertThat(map[i]).isEqualTo(expectedSnapPositions[i]) }
+        repeat(itemCount) { i ->
+            assertThat(getSnapPositionOffset(strategy, i, itemCount))
+                .isEqualTo(expectedSnapPositions[i])
+        }
     }
 
     @Test
@@ -53,9 +62,11 @@
         val strategy = testStartAlignedStrategyWithMultipleFocal()
         // item count is the number of keylines minus anchor keylines
         val itemCount = strategy.defaultKeylines.size - 2
-        val map = calculateSnapPositions(strategy, itemCount)
         val expectedSnapPositions = arrayListOf(0, 75, 200, 200)
-        repeat(itemCount) { i -> assertThat(map[i]).isEqualTo(expectedSnapPositions[i]) }
+        repeat(itemCount) { i ->
+            assertThat(getSnapPositionOffset(strategy, i, itemCount))
+                .isEqualTo(expectedSnapPositions[i])
+        }
     }
 
     @Test
@@ -64,8 +75,9 @@
         // item count is the number of focal keylines minus one
         val itemCount = strategy.defaultKeylines.lastFocalIndex -
             strategy.defaultKeylines.firstFocalIndex
-        val map = calculateSnapPositions(strategy, itemCount)
-        repeat(itemCount) { i -> assertThat(map[i]).isEqualTo(0) }
+        repeat(itemCount) { i ->
+            assertThat(getSnapPositionOffset(strategy, i, itemCount)).isEqualTo(0)
+        }
     }
 
     // Test strategy that is center aligned and has a complex keyline state, ie:
@@ -97,7 +109,8 @@
             add(xSmallSize, isAnchor = true)
         }
 
-        return Strategy { _, _ -> keylineList }.apply(
+        return Strategy(
+            defaultKeylines = keylineList,
             availableSpace = 1000f,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -132,7 +145,8 @@
             add(smallSize)
             add(xSmallSize, isAnchor = true)
         }
-        return Strategy { _, _ -> keylineList }.apply(
+        return Strategy(
+            defaultKeylines = keylineList,
             availableSpace = 1000f,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -160,7 +174,8 @@
             add(smallSize)
             add(xSmallSize, isAnchor = true)
         }
-        return Strategy { _, _ -> keylineList }.apply(
+        return Strategy(
+            defaultKeylines = keylineList,
             availableSpace = 1000f,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
index 830559f..6f829c0 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
@@ -38,8 +38,9 @@
             preferredItemSize = itemSize,
             itemSpacing = 0f,
             itemCount = 10,
-        )!!
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+        )
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = 500f,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -58,8 +59,9 @@
             preferredItemSize = itemSize,
             itemSpacing = 0f,
             itemCount = 10,
-            )!!
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+            )
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = 100f,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -87,8 +89,9 @@
             preferredItemSize = 200f,
             itemSpacing = 0f,
             itemCount = 10,
-            )!!
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+            )
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = minSmallItemSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -110,7 +113,7 @@
             itemSpacing = 0f,
             itemCount = 10,
             )
-        assertThat(keylineList).isNull()
+        assertThat(keylineList).isEmpty()
     }
 
     @Test
@@ -124,8 +127,9 @@
             preferredItemSize = preferredItemSize,
             itemSpacing = 0f,
             itemCount = 10,
-            )!!
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+            )
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = carouselSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -154,8 +158,9 @@
             preferredItemSize = preferredItemSize,
             itemSpacing = 0f,
             itemCount = 3,
-        )!!
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+        )
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = carouselSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -179,8 +184,9 @@
             preferredItemSize = 186f,
             itemSpacing = 8f,
             itemCount = 10
-        )!!
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+        )
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = 380f,
             itemSpacing = 8f,
             beforeContentPadding = 0f,
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
index e19e370..1606293 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
@@ -34,7 +34,8 @@
         val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
         val defaultKeylineList = createStartAlignedKeylineList()
 
-        val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+        val strategy = Strategy(
+            defaultKeylines = defaultKeylineList,
             availableSpace = carouselMainAxisSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -67,13 +68,14 @@
         val cutoff = 50f
         val defaultKeylineList = createStartAlignedCutoffKeylineList(cutoff = cutoff)
 
-        val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+        val strategy = Strategy(
+            defaultKeylines = defaultKeylineList,
             availableSpace = carouselMainAxisSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
         )
-        val endKeylineList = strategy.getEndKeylines()
+        val endKeylineList = strategy.endKeylineSteps.last()
 
         assertThat(defaultKeylineList.lastNonAnchor.cutoff).isEqualTo(cutoff)
         assertThat(defaultKeylineList.firstNonAnchor.offset -
@@ -91,13 +93,14 @@
         val cutoff = 50f
         val defaultKeylineList = createEndAlignedCutoffKeylineList(cutoff = cutoff)
 
-        val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+        val strategy = Strategy(
+            defaultKeylines = defaultKeylineList,
             availableSpace = carouselMainAxisSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
         )
-        val startKeylineList = strategy.getStartKeylines()
+        val startKeylineList = strategy.startKeylineSteps.last()
 
         assertThat(defaultKeylineList.firstNonAnchor.cutoff).isEqualTo(cutoff)
         assertThat(defaultKeylineList.lastNonAnchor.offset +
@@ -116,7 +119,8 @@
         val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
         val defaultKeylines = createCenterAlignedKeylineList()
 
-        val strategy = Strategy { _, _ -> defaultKeylines }.apply(
+        val strategy = Strategy(
+            defaultKeylines = defaultKeylines,
             availableSpace = carouselMainAxisSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -225,7 +229,8 @@
         val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
         val defaultKeylines = createCenterAlignedKeylineList()
 
-        val strategy = Strategy { _, _ -> defaultKeylines }.apply(
+        val strategy = Strategy(
+            defaultKeylines = defaultKeylines,
             availableSpace = carouselMainAxisSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -357,21 +362,20 @@
     fun testStrategy_sameAvailableSpaceCreatesEqualObjects() {
         val itemSize = large
         val itemCount = 10
+        val availableSpace = 500f
         val itemSpacing = 0f
-        val strategy1 = Strategy { availableSpace, itemSpacingPx ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
-        }
-        val strategy2 = Strategy { availableSpace, itemSpacingPx ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
-        }
-        strategy1.apply(
-            availableSpace = 500f,
+        val strategy1 = Strategy(
+            defaultKeylines =
+                multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount),
+            availableSpace = availableSpace,
             itemSpacing = itemSpacing,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
         )
-        strategy2.apply(
-            availableSpace = 500f,
+        val strategy2 = Strategy(
+            defaultKeylines =
+                multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount),
+            availableSpace = availableSpace,
             itemSpacing = itemSpacing,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
@@ -386,20 +390,18 @@
         val itemSize = large
         val itemSpacing = 0f
         val itemCount = 10
-        val strategy1 = Strategy { availableSpace, itemSpacingPx ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
-        }
-        val strategy2 = Strategy { availableSpace, itemSpacingPx ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
-        }
-        strategy1.apply(
+        val strategy1 = Strategy(
+            defaultKeylines =
+                multiBrowseKeylineList(Density, 500f, itemSize, itemSpacing, itemCount),
             availableSpace = 500f,
             itemSpacing = itemSpacing,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
         )
-        strategy2.apply(
-            availableSpace = 500f + 1f,
+        val strategy2 = Strategy(
+            defaultKeylines =
+                multiBrowseKeylineList(Density, 501f, itemSize, itemSpacing, itemCount),
+            availableSpace = 501f,
             itemSpacing = itemSpacing,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
@@ -412,16 +414,20 @@
     @Test
     fun testStrategy_invalidObjectDoesNotEqualValidObject() {
         val itemSize = large
+        val availableSpace = 500f
         val itemSpacing = 0f
         val itemCount = 10
-        val strategy1 = Strategy { availableSpace, itemSpacingPx ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
-        }
-        val strategy2 = Strategy { availableSpace, itemSpacingPx ->
-            multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
-        }
-        strategy1.apply(
-            availableSpace = 500f,
+        val strategy1 = Strategy(
+            defaultKeylines =
+                multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacing, itemCount),
+            availableSpace = availableSpace,
+            itemSpacing = itemSpacing,
+            beforeContentPadding = 0f,
+            afterContentPadding = 0f
+        )
+        val strategy2 = Strategy(
+            defaultKeylines = emptyKeylineList(),
+            availableSpace = availableSpace,
             itemSpacing = itemSpacing,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
@@ -438,7 +444,8 @@
         val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
         val defaultKeylineList = createStartAlignedKeylineList()
 
-        val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+        val strategy = Strategy(
+            defaultKeylines = defaultKeylineList,
             availableSpace = carouselMainAxisSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -451,17 +458,19 @@
 
     @Test
     fun testStartKeylineStrategy_endStepsShouldAccountForItemSpacing() {
-        val strategy = Strategy { availableSpace, itemSpacing ->
-            keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Start) {
-                add(10f, isAnchor = true)
-                add(186f)
-                add(122f)
-                add(56f)
-                add(10f, isAnchor = true)
-            }
-        }.apply(
-            availableSpace = 380f,
-            itemSpacing = 8f,
+        val availableSpace = 380f
+        val itemSpacing = 8f
+        val strategy = Strategy(
+            defaultKeylines =
+                keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Start) {
+                    add(10f, isAnchor = true)
+                    add(186f)
+                    add(122f)
+                    add(56f)
+                    add(10f, isAnchor = true)
+                },
+            availableSpace = availableSpace,
+            itemSpacing = itemSpacing,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
         )
@@ -487,8 +496,10 @@
 
     @Test
     fun testCenterKeylineStrategy_startAndEndStepsShouldAccountForItemSpacing() {
-        val strategy = Strategy { availableSpace, itemSpacing ->
-            keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Center) {
+        val availableSpace = 768f
+        val itemSpacing = 8f
+        val strategy = Strategy(
+            defaultKeylines = keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Center) {
                 add(10f, isAnchor = true)
                 add(56f)
                 add(122f)
@@ -497,10 +508,9 @@
                 add(122f)
                 add(56f)
                 add(10f, isAnchor = true)
-            }
-        }.apply(
-            availableSpace = 768f,
-            itemSpacing = 8f,
+            },
+            availableSpace = availableSpace,
+            itemSpacing = itemSpacing,
             beforeContentPadding = 0f,
             afterContentPadding = 0f
         )
@@ -559,17 +569,24 @@
 
     @Test
     fun testCenterStrategy_stepsShouldAccountForContentPadding() {
-        val strategy = Strategy { availableSpace, itemSpacing ->
-            keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Center) {
-                add(10f, isAnchor = true)
-                add(50f)
-                add(100f)
-                add(200f)
-                add(100f)
-                add(50f)
-                add(10f, isAnchor = true)
-            }
-        }.apply(500f, 0f, 16f, 24f)
+        val availableSpace = 500f
+        val itemSpacing = 0f
+        val strategy = Strategy(
+            defaultKeylines =
+                keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Center) {
+                    add(10f, isAnchor = true)
+                    add(50f)
+                    add(100f)
+                    add(200f)
+                    add(100f)
+                    add(50f)
+                    add(10f, isAnchor = true)
+                },
+            availableSpace = availableSpace,
+            itemSpacing = itemSpacing,
+            beforeContentPadding = 16f,
+            afterContentPadding = 24f
+        )
 
         val lastStartStep = strategy.startKeylineSteps.last()
 
@@ -591,17 +608,19 @@
 
     @Test
     fun testStartStrategy_twoLargeOneSmall_shouldAccountForPadding() {
-        val strategy = Strategy { availableSpace, itemSpacing ->
-            keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Start) {
-                add(10f, isAnchor = true)
-                add(186f)
-                add(186f)
-                add(56f)
-                add(10f, isAnchor = true)
-            }
-        }.apply(
-            availableSpace = 444f,
-            itemSpacing = 8f,
+        val availableSpace = 444f
+        val itemSpacing = 8f
+        val strategy = Strategy(
+            defaultKeylines =
+                keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Start) {
+                    add(10f, isAnchor = true)
+                    add(186f)
+                    add(186f)
+                    add(56f)
+                    add(10f, isAnchor = true)
+                },
+            availableSpace = availableSpace,
+            itemSpacing = itemSpacing,
             beforeContentPadding = 16f,
             afterContentPadding = 16f
         )
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
index cf6aa49..1a74bad 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
@@ -39,7 +39,8 @@
             itemSize = itemSize,
             itemSpacing = 0f
         )
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = carouselSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -67,7 +68,8 @@
             itemSize = itemSize,
             itemSpacing = 0f
         )
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = carouselSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -98,7 +100,8 @@
             itemSize = itemSize,
             itemSpacing = 0f
         )
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = carouselSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
@@ -138,7 +141,8 @@
             itemSize = itemSize,
             itemSpacing = 0f
         )
-        val strategy = Strategy { _, _ -> keylineList }.apply(
+        val strategy = Strategy(
+            defaultKeylines = keylineList,
             availableSpace = carouselSize,
             itemSpacing = 0f,
             beforeContentPadding = 0f,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
index 0930a98..d4e817a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
@@ -21,18 +21,23 @@
 import androidx.compose.material3.tokens.DefaultTextStyle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.structuralEqualityPolicy
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.takeOrElse
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.LinkInteractionListener
 import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.fromHtml
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.text.style.TextOverflow
@@ -351,3 +356,76 @@
     val mergedStyle = LocalTextStyle.current.merge(value)
     CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
 }
+
+/** Contains the methods to be used by [Text] */
+object TextDefaults {
+    /**
+     * Converts a string with HTML tags into [AnnotatedString]. Applies default styling from the
+     * [MaterialTheme] to links present in the [htmlString].
+     *
+     * Check [androidx.compose.ui.text.AnnotatedString.Companion.fromHtml] for more details on
+     * supported tags and usage.
+     *
+     * @param htmlString HTML-tagged string to be parsed to construct AnnotatedString
+     * @param linkStyle style to be applied to links present in the string
+     * @param linkFocusedStyle style to be applied to links present in the string when they are
+     * focused
+     * @param linkHoveredStyle style to be applied to links present in the string when they are
+     * hovered
+     * @param linkInteractionListener a listener that will be attached to links that are present in
+     * the string and triggered when user clicks on those links. When set to null, which is
+     * a default, the system will try to open the corresponding links with the
+     * [androidx.compose.ui.platform.UriHandler] composition local
+     *
+     * @see androidx.compose.ui.text.AnnotatedString.Companion.fromHtml
+     */
+    @Composable
+    @ReadOnlyComposable
+    fun fromHtml(
+        htmlString: String,
+        linkStyle: SpanStyle? = SpanStyle(color = MaterialTheme.colorScheme.primary),
+        linkFocusedStyle: SpanStyle? = null,
+        linkHoveredStyle: SpanStyle? = null,
+        linkInteractionListener: LinkInteractionListener? = null
+    ): AnnotatedString {
+        return AnnotatedString.fromHtml(
+            htmlString, linkStyle, linkFocusedStyle, linkHoveredStyle, linkInteractionListener
+        )
+    }
+
+    /**
+     * Constructs a [LinkAnnotation.Url] and applies default styling from the [MaterialTheme]
+     *
+     * @sample androidx.compose.material3.samples.AnnotatedStringWithLinks
+     */
+    @Composable
+    @ReadOnlyComposable
+    fun Url(
+        url: String,
+        linkStyle: SpanStyle? = SpanStyle(color = MaterialTheme.colorScheme.primary),
+        linkFocusedStyle: SpanStyle? = null,
+        linkHoveredStyle: SpanStyle? = null,
+        linkInteractionListener: LinkInteractionListener? = null
+    ): LinkAnnotation.Url {
+        return LinkAnnotation.Url(
+            url, linkStyle, linkFocusedStyle, linkHoveredStyle, linkInteractionListener
+        )
+    }
+
+    /**
+     * Constructs a [LinkAnnotation.Clickable] and applies default styling from the [MaterialTheme]
+     */
+    @Composable
+    @ReadOnlyComposable
+    fun Clickable(
+        tag: String,
+        linkStyle: SpanStyle? = SpanStyle(color = MaterialTheme.colorScheme.primary),
+        linkFocusedStyle: SpanStyle? = null,
+        linkHoveredStyle: SpanStyle? = null,
+        linkInteractionListener: LinkInteractionListener?
+    ): LinkAnnotation.Clickable {
+        return LinkAnnotation.Clickable(
+            tag, linkStyle, linkFocusedStyle, linkHoveredStyle, linkInteractionListener
+        )
+    }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index 98413b8..a690b6b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -646,7 +646,6 @@
     internal var selection by mutableStateOf(Selection.Hour)
     internal var isAfternoonToggle by mutableStateOf(initialHour >= 12 && !is24Hour)
     internal var isInnerCircle by mutableStateOf(initialHour >= 12)
-
     internal var hourAngle by mutableFloatStateOf(
         RadiansPerHour * (initialHour % 12) - FullCircle / 4
     )
@@ -655,17 +654,24 @@
     )
 
     private val mutex = MutatorMutex()
-    private val isAfternoon by derivedStateOf { is24hour && isInnerCircle || isAfternoonToggle }
+    private val isAfternoon
+        get() = is24hour && isInnerCircle || isAfternoonToggle
 
-    internal val currentAngle = Animatable(hourAngle)
+    internal var currentAngle = Animatable(hourAngle)
 
     internal fun setMinute(minute: Int) {
         minuteAngle = RadiansPerMinute * minute - FullCircle / 4
+        if (selection == Selection.Minute) {
+            currentAngle = Animatable(minuteAngle)
+        }
     }
 
     internal fun setHour(hour: Int) {
         isInnerCircle = hour >= 12
         hourAngle = RadiansPerHour * (hour % 12) - FullCircle / 4
+        if (selection == Selection.Hour) {
+            currentAngle = Animatable(hourAngle)
+        }
     }
 
     internal fun moveSelector(x: Float, y: Float, maxDist: Float) {
@@ -674,13 +680,6 @@
         }
     }
 
-    internal fun isSelected(value: Int): Boolean =
-        if (selection == Selection.Minute) {
-            value == minute
-        } else {
-            hour == (value + if (isAfternoon) 12 else 0)
-        }
-
     internal suspend fun update(value: Float, fromTap: Boolean = false) {
         mutex.mutate(MutatePriority.UserInput) {
             if (selection == Selection.Hour) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index 65effa4..8984250 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -17,7 +17,6 @@
 package androidx.compose.material3.carousel
 
 import androidx.annotation.VisibleForTesting
-import androidx.collection.IntIntMap
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.DecayAnimationSpec
 import androidx.compose.animation.core.Spring
@@ -41,7 +40,10 @@
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ShapeDefaults
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.Rect
@@ -49,7 +51,6 @@
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Outline
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
@@ -57,9 +58,6 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFilter
-import androidx.compose.ui.util.fastForEach
-import kotlin.math.ceil
 import kotlin.math.roundToInt
 
 /**
@@ -132,6 +130,9 @@
             }
         },
         contentPadding = contentPadding,
+        // 2 is the max number of medium and small items that can be present in a multi-browse
+        // carousel and should be the upper bounds max non focal visible items.
+        maxNonFocalVisibleItemCount = 2,
         modifier = modifier,
         itemSpacing = itemSpacing,
         flingBehavior = flingBehavior,
@@ -191,6 +192,9 @@
             }
         },
         contentPadding = contentPadding,
+        // Since uncontained carousels only have one item that masks as it moves in/out of view,
+        // there is no need to increase the max non focal count.
+        maxNonFocalVisibleItemCount = 0,
         modifier = modifier,
         itemSpacing = itemSpacing,
         flingBehavior = flingBehavior,
@@ -209,6 +213,9 @@
  * @param keylineList The list of keylines that are fixed positions along the scrolling axis which
  * define the state an item should be in when its center is co-located with the keyline's position.
  * @param contentPadding a padding around the whole content. This will add padding for the
+ * @param maxNonFocalVisibleItemCount the maximum number of items that are visible but not fully
+ * unmasked (focal) at one time. This number helps determine how many items should be composed
+ * to fill the entire viewport.
  * @param modifier A modifier instance to be applied to this carousel outer layout
  * content after it has been clipped. You can use it to add a padding before the first item or
  * after the last one. Use [itemSpacing] to add spacing between the items.
@@ -222,32 +229,22 @@
 internal fun Carousel(
     state: CarouselState,
     orientation: Orientation,
-    keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?,
+    keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList,
     contentPadding: PaddingValues,
+    maxNonFocalVisibleItemCount: Int,
     modifier: Modifier = Modifier,
     itemSpacing: Dp = 0.dp,
     flingBehavior: TargetedFlingBehavior =
         CarouselDefaults.singleAdvanceFlingBehavior(state = state),
     content: @Composable CarouselItemScope.(itemIndex: Int) -> Unit
 ) {
-    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
     val beforeContentPadding = contentPadding.calculateBeforeContentPadding(orientation)
     val afterContentPadding = contentPadding.calculateAfterContentPadding(orientation)
     val pageSize = remember(keylineList) {
         CarouselPageSize(keylineList, beforeContentPadding, afterContentPadding)
     }
 
-    val beyondViewportPageCount = remember(pageSize.strategy.itemMainAxisSize) {
-        calculateBeyondViewportPageCount(pageSize.strategy)
-    }
-
-    val snapPositionMap = remember(pageSize.strategy.itemMainAxisSize) {
-        calculateSnapPositions(
-            pageSize.strategy,
-            state.itemCountState.value()
-        )
-    }
-    val snapPosition = remember(snapPositionMap) { KeylineSnapPosition(snapPositionMap) }
+    val snapPosition = KeylineSnapPosition(pageSize)
 
     if (orientation == Orientation.Horizontal) {
         HorizontalPager(
@@ -259,7 +256,7 @@
             ),
             pageSize = pageSize,
             pageSpacing = itemSpacing,
-            beyondViewportPageCount = beyondViewportPageCount,
+            beyondViewportPageCount = maxNonFocalVisibleItemCount,
             snapPosition = snapPosition,
             flingBehavior = flingBehavior,
             modifier = modifier
@@ -271,10 +268,8 @@
                 modifier = Modifier.carouselItem(
                     index = page,
                     state = state,
-                    strategy = pageSize.strategy,
-                    itemPositionMap = snapPositionMap,
+                    strategy = { pageSize.strategy },
                     carouselItemInfo = carouselItemInfo,
-                    isRtl = isRtl
                 )
             ) {
                 scope.content(page)
@@ -290,7 +285,7 @@
             ),
             pageSize = pageSize,
             pageSpacing = itemSpacing,
-            beyondViewportPageCount = beyondViewportPageCount,
+            beyondViewportPageCount = maxNonFocalVisibleItemCount,
             snapPosition = snapPosition,
             flingBehavior = flingBehavior,
             modifier = modifier
@@ -302,10 +297,8 @@
                 modifier = Modifier.carouselItem(
                     index = page,
                     state = state,
-                    strategy = pageSize.strategy,
-                    itemPositionMap = snapPositionMap,
+                    strategy = { pageSize.strategy },
                     carouselItemInfo = carouselItemInfo,
-                    isRtl = isRtl
                 )
             ) {
                 scope.content(page)
@@ -336,23 +329,6 @@
     return with(LocalDensity.current) { dpValue.toPx() }
 }
 
-internal fun calculateBeyondViewportPageCount(strategy: Strategy): Int {
-    if (!strategy.isValid()) {
-        return PagerDefaults.BeyondViewportPageCount
-    }
-    var totalKeylineSpace = 0f
-    var totalNonAnchorKeylines = 0
-    strategy.defaultKeylines.fastFilter { !it.isAnchor }.fastForEach {
-        totalKeylineSpace += it.size
-        totalNonAnchorKeylines += 1
-    }
-    val itemsLoaded = ceil(totalKeylineSpace / strategy.itemMainAxisSize).toInt()
-    val itemsToLoad = totalNonAnchorKeylines - itemsLoaded
-
-    // We must also load the next item when scrolling
-    return itemsToLoad + 1
-}
-
 /**
  * A [PageSize] implementation that maintains a strategy that is kept up-to-date with the
  * latest available space of the container.
@@ -360,24 +336,31 @@
  * @param keylineList The list of keylines that are fixed positions along the scrolling axis which
  * define the state an item should be in when its center is co-located with the keyline's position.
  */
-private class CarouselPageSize(
-    keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?,
+internal class CarouselPageSize(
+    private val keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList,
     private val beforeContentPadding: Float,
     private val afterContentPadding: Float
 ) : PageSize {
-    val strategy = Strategy(keylineList)
+
+    private var strategyState by mutableStateOf(Strategy.Empty)
+    val strategy: Strategy
+        get() = strategyState
+
     override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
-        strategy.apply(
+        val keylines = keylineList.invoke(availableSpace.toFloat(), pageSpacing.toFloat())
+        strategyState = Strategy(
+            keylines,
             availableSpace.toFloat(),
             pageSpacing.toFloat(),
             beforeContentPadding,
             afterContentPadding
         )
-        return if (strategy.isValid()) {
+
+        // If a valid strategy is available, use the strategy's item size. Otherwise, default to
+        // a full size item as Pager does by default.
+        return if (strategy.isValid) {
             strategy.itemMainAxisSize.roundToInt()
         } else {
-            // If strategy does not have a valid arrangement, default to a
-            // full size item, as Pager does by default.
             availableSpace
         }
     }
@@ -407,25 +390,27 @@
  * @param index the index of the item in the carousel
  * @param state the carousel state
  * @param strategy the strategy used to mask and translate items in the carousel
- * @param itemPositionMap the position of each index when it is the current item
  * @param carouselItemInfo the item info that should be updated with the changes in this modifier
- * @param isRtl true if the layout direction is right-to-left
  */
 @OptIn(ExperimentalMaterial3Api::class)
 internal fun Modifier.carouselItem(
     index: Int,
     state: CarouselState,
-    strategy: Strategy,
-    itemPositionMap: IntIntMap,
+    strategy: () -> Strategy,
     carouselItemInfo: CarouselItemInfoImpl,
-    isRtl: Boolean
 ): Modifier {
-    if (!strategy.isValid()) return this
-    val isVertical = state.pagerState.layoutInfo.orientation == Orientation.Vertical
-
     return layout { measurable, constraints ->
+        val strategyResult = strategy.invoke()
+        if (!strategyResult.isValid) {
+            // If there is no strategy, avoid displaying content
+            return@layout layout(0, 0) { }
+        }
+
+        val isVertical = state.pagerState.layoutInfo.orientation == Orientation.Vertical
+        val isRtl = layoutDirection == LayoutDirection.Rtl
+
         // Force the item to use the strategy's itemMainAxisSize along its main axis
-        val mainAxisSize = strategy.itemMainAxisSize
+        val mainAxisSize = strategyResult.itemMainAxisSize
         val itemConstraints = if (isVertical) {
             constraints.copy(
                 minWidth = constraints.minWidth,
@@ -444,97 +429,103 @@
 
         val placeable = measurable.measure(itemConstraints)
         layout(placeable.width, placeable.height) {
-            placeable.place(0, 0)
-        }
-    }.graphicsLayer {
-        val scrollOffset = calculateCurrentScrollOffset(state, strategy, itemPositionMap)
-        val maxScrollOffset = calculateMaxScrollOffset(state, strategy)
-        // TODO: Reduce the number of times a keyline for the same scroll offset is calculated
-        val keylines = strategy.getKeylineListForScrollOffset(scrollOffset, maxScrollOffset)
-        val roundedKeylines = strategy.getKeylineListForScrollOffset(
-            scrollOffset = scrollOffset,
-            maxScrollOffset = maxScrollOffset,
-            roundToNearestStep = true
-        )
-
-        // Find center of the item at this index
-        val itemSizeWithSpacing = strategy.itemMainAxisSize + strategy.itemSpacing
-        val unadjustedCenter =
-            (index * itemSizeWithSpacing) + (strategy.itemMainAxisSize / 2f) - scrollOffset
-
-        // Find the keyline before and after this item's center and create an interpolated
-        // keyline that the item should use for its clip shape and offset
-        val keylineBefore =
-            keylines.getKeylineBefore(unadjustedCenter)
-        val keylineAfter =
-            keylines.getKeylineAfter(unadjustedCenter)
-        val progress = getProgress(keylineBefore, keylineAfter, unadjustedCenter)
-        val interpolatedKeyline = lerp(keylineBefore, keylineAfter, progress)
-        val isOutOfKeylineBounds = keylineBefore == keylineAfter
-
-        val centerX =
-            if (isVertical) size.height / 2f else strategy.itemMainAxisSize / 2f
-        val centerY =
-            if (isVertical) strategy.itemMainAxisSize / 2f else size.height / 2f
-        val halfMaskWidth =
-            if (isVertical) size.width / 2f else interpolatedKeyline.size / 2f
-        val halfMaskHeight =
-            if (isVertical) interpolatedKeyline.size / 2f else size.height / 2f
-        val maskRect = Rect(
-            left = centerX - halfMaskWidth,
-            top = centerY - halfMaskHeight,
-            right = centerX + halfMaskWidth,
-            bottom = centerY + halfMaskHeight
-        )
-
-        // Update carousel item info
-        carouselItemInfo.sizeState.floatValue = interpolatedKeyline.size
-        carouselItemInfo.minSizeState.floatValue = roundedKeylines.minBy { it.size }.size
-        carouselItemInfo.maxSizeState.floatValue = roundedKeylines.firstFocal.size
-
-        // Clip the item
-        clip = true
-        shape = object : Shape {
-            // TODO: Find a way to use the shape of the item set by the client for each item
-            // TODO: Allow corner size customization
-            val roundedCornerShape = RoundedCornerShape(ShapeDefaults.ExtraLarge.topStart)
-            override fun createOutline(
-                size: Size,
-                layoutDirection: LayoutDirection,
-                density: Density
-            ): Outline {
-                val cornerSize =
-                    roundedCornerShape.topStart.toPx(
-                        Size(maskRect.width, maskRect.height),
-                        density
-                    )
-                val cornerRadius = CornerRadius(cornerSize)
-                return Outline.Rounded(
-                    RoundRect(
-                        rect = maskRect,
-                        topLeft = cornerRadius,
-                        topRight = cornerRadius,
-                        bottomRight = cornerRadius,
-                        bottomLeft = cornerRadius
-                    )
+            placeable.placeWithLayer(0, 0, layerBlock = {
+                val scrollOffset = calculateCurrentScrollOffset(state, strategyResult)
+                val maxScrollOffset = calculateMaxScrollOffset(state, strategyResult)
+                // TODO: Reduce the number of times keylins are calculated
+                val keylines = strategyResult.getKeylineListForScrollOffset(
+                    scrollOffset,
+                    maxScrollOffset
                 )
-            }
-        }
+                val roundedKeylines = strategyResult.getKeylineListForScrollOffset(
+                    scrollOffset = scrollOffset,
+                    maxScrollOffset = maxScrollOffset,
+                    roundToNearestStep = true
+                )
 
-        // After clipping, the items will have white space between them. Translate the items to
-        // pin their edges together
-        var translation = interpolatedKeyline.offset - unadjustedCenter
-        if (isOutOfKeylineBounds) {
-            // If this item is beyond the first or last keyline, continue to offset the item
-            // by cutting its unadjustedOffset according to its masked size.
-            val outOfBoundsOffset = (unadjustedCenter - interpolatedKeyline.unadjustedOffset) /
-                interpolatedKeyline.size
-            translation += outOfBoundsOffset
-        }
-        if (isVertical) {
-            translationY = translation
-        } else {
-            translationX = if (isRtl) -translation else translation
+                // Find center of the item at this index
+                val itemSizeWithSpacing = strategyResult.itemMainAxisSize +
+                    strategyResult.itemSpacing
+                val unadjustedCenter = (index * itemSizeWithSpacing) +
+                    (strategyResult.itemMainAxisSize / 2f) - scrollOffset
+
+                // Find the keyline before and after this item's center and create an interpolated
+                // keyline that the item should use for its clip shape and offset
+                val keylineBefore =
+                    keylines.getKeylineBefore(unadjustedCenter)
+                val keylineAfter =
+                    keylines.getKeylineAfter(unadjustedCenter)
+                val progress = getProgress(keylineBefore, keylineAfter, unadjustedCenter)
+                val interpolatedKeyline = lerp(keylineBefore, keylineAfter, progress)
+                val isOutOfKeylineBounds = keylineBefore == keylineAfter
+
+                val centerX =
+                    if (isVertical) size.height / 2f else strategyResult.itemMainAxisSize / 2f
+                val centerY =
+                    if (isVertical) strategyResult.itemMainAxisSize / 2f else size.height / 2f
+                val halfMaskWidth =
+                    if (isVertical) size.width / 2f else interpolatedKeyline.size / 2f
+                val halfMaskHeight =
+                    if (isVertical) interpolatedKeyline.size / 2f else size.height / 2f
+                val maskRect = Rect(
+                    left = centerX - halfMaskWidth,
+                    top = centerY - halfMaskHeight,
+                    right = centerX + halfMaskWidth,
+                    bottom = centerY + halfMaskHeight
+                )
+
+                // Update carousel item info
+                carouselItemInfo.sizeState = interpolatedKeyline.size
+                carouselItemInfo.minSizeState = roundedKeylines.minBy { it.size }.size
+                carouselItemInfo.maxSizeState = roundedKeylines.firstFocal.size
+                carouselItemInfo.maskRectState = maskRect
+
+                // Clip the item
+                clip = true
+                shape = object : Shape {
+                    // TODO: Find a way to use the shape of the item set by the client for each item
+                    // TODO: Allow corner size customization
+                    val roundedCornerShape = RoundedCornerShape(ShapeDefaults.ExtraLarge.topStart)
+                    override fun createOutline(
+                        size: Size,
+                        layoutDirection: LayoutDirection,
+                        density: Density
+                    ): Outline {
+                        val cornerSize =
+                            roundedCornerShape.topStart.toPx(
+                                Size(maskRect.width, maskRect.height),
+                                density
+                            )
+                        val cornerRadius = CornerRadius(cornerSize)
+                        return Outline.Rounded(
+                            RoundRect(
+                                rect = maskRect,
+                                topLeft = cornerRadius,
+                                topRight = cornerRadius,
+                                bottomRight = cornerRadius,
+                                bottomLeft = cornerRadius
+                            )
+                        )
+                    }
+                }
+
+                // After clipping, the items will have white space between them. Translate the
+                // items to pin their edges together
+                var translation = interpolatedKeyline.offset - unadjustedCenter
+                if (isOutOfKeylineBounds) {
+                    // If this item is beyond the first or last keyline, continue to offset the
+                    // item by cutting its unadjustedOffset according to its masked size.
+                    val outOfBoundsOffset =
+                        (unadjustedCenter - interpolatedKeyline.unadjustedOffset) /
+                            interpolatedKeyline.size
+                    translation += outOfBoundsOffset
+                }
+                if (isVertical) {
+                    translationY = translation
+                } else {
+                    translationX = if (isRtl) -translation else translation
+                }
+            })
         }
     }
 }
@@ -544,14 +535,13 @@
 internal fun calculateCurrentScrollOffset(
     state: CarouselState,
     strategy: Strategy,
-    snapPositionMap: IntIntMap
 ): Float {
     val itemSizeWithSpacing = strategy.itemMainAxisSize + strategy.itemSpacing
     val currentItemScrollOffset =
         (state.pagerState.currentPage * itemSizeWithSpacing) +
             (state.pagerState.currentPageOffsetFraction * itemSizeWithSpacing)
     return currentItemScrollOffset -
-        (if (snapPositionMap.size > 0) snapPositionMap[state.pagerState.currentPage] else 0)
+        getSnapPositionOffset(strategy, state.pagerState.currentPage, state.pagerState.pageCount)
 }
 
 /** Returns the max scroll offset given the item count, sizing, and spacing. */
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
index 1f5f990..0e45d2b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
@@ -23,11 +23,14 @@
 import androidx.compose.foundation.pager.PagerState
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Rect
 
 /**
  * The state that can be used to control all types of carousels.
@@ -133,56 +136,28 @@
      * at a focal position
      */
     val maxSize: Float
-}
 
-/**
- * Gets the start offset of the carousel item from its full size. This offset can be used to pin any
- * carousel item content to the left side of the item (or right if RTL).
- */
-@OptIn(ExperimentalMaterial3Api::class)
-fun CarouselItemInfo.startOffset(): Float {
-    return (maxSize - size) / 2f
-}
-
-/**
- * Gets the end offset of the carousel item from its full size. This offset can be used to pin any
- * carousel item content to the right side of the item (or left if RTL).
- */
-@OptIn(ExperimentalMaterial3Api::class)
-fun CarouselItemInfo.endOffset(): Float {
-    return maxSize - (maxSize - size) / 2f
+    /** The rect by which the carousel item is being clipped. */
+    val maskRect: Rect
 }
 
 @OptIn(ExperimentalMaterial3Api::class)
 internal class CarouselItemInfoImpl : CarouselItemInfo {
 
-    val sizeState = mutableFloatStateOf(0f)
+    var sizeState by mutableFloatStateOf(0f)
+    var minSizeState by mutableFloatStateOf(0f)
+    var maxSizeState by mutableFloatStateOf(0f)
+    var maskRectState by mutableStateOf(Rect.Zero)
+
     override val size: Float
-        get() = sizeState.floatValue
+        get() = sizeState
 
-    val minSizeState = mutableFloatStateOf(0f)
     override val minSize: Float
-        get() = minSizeState.floatValue
+        get() = minSizeState
 
-    val maxSizeState = mutableFloatStateOf(0f)
     override val maxSize: Float
-        get() = maxSizeState.floatValue
+        get() = maxSizeState
 
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is CarouselItemInfoImpl) return false
-
-        if (sizeState != other.sizeState) return false
-        if (minSizeState != other.minSizeState) return false
-        if (maxSizeState != other.maxSizeState) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = sizeState.hashCode()
-        result = 31 * result + minSizeState.hashCode()
-        result = 31 * result + maxSizeState.hashCode()
-        return result
-    }
+    override val maskRect: Rect
+        get() = maskRectState
 }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineList.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineList.kt
index a0125af..0f2acee 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineList.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineList.kt
@@ -74,7 +74,8 @@
     val pivotIndex: Int = indexOfFirst { it.isPivot }
 
     /** Returns the keyline used to calculate all other keyline offsets and unadjusted offsets. */
-    val pivot: Keyline = get(pivotIndex)
+    val pivot: Keyline
+        get() = get(pivotIndex)
 
     /**
      * Returns the index of the first non-anchor keyline or -1 if the list does not contain a
@@ -86,7 +87,8 @@
      * Returns the first non-anchor [Keyline].
      * @throws [NoSuchElementException] if there are no non-anchor keylines.
      */
-    val firstNonAnchor: Keyline = get(firstNonAnchorIndex)
+    val firstNonAnchor: Keyline
+        get() = get(firstNonAnchorIndex)
 
     /**
      * Returns the index of the last non-anchor keyline or -1 if the list does not contain a
@@ -98,7 +100,8 @@
      * Returns the last non-anchor [Keyline].
      * @throws [NoSuchElementException] if there are no non-anchor keylines.
      */
-    val lastNonAnchor = get(lastNonAnchorIndex)
+    val lastNonAnchor: Keyline
+        get() = get(lastNonAnchorIndex)
 
     /**
      * Returns the index of the first focal keyline or -1 if the list does not contain a
@@ -110,8 +113,9 @@
      * Returns the first focal [Keyline].
      * @throws [NoSuchElementException] if there are no focal keylines.
      */
-    val firstFocal: Keyline = getOrNull(firstFocalIndex)
-        ?: throw NoSuchElementException("All KeylineLists must have at least one focal keyline")
+    val firstFocal: Keyline
+        get() = getOrNull(firstFocalIndex)
+            ?: throw NoSuchElementException("All KeylineLists must have at least one focal keyline")
 
     /**
      * Returns the index of the last focal keyline or -1 if the list does not contain a
@@ -123,8 +127,9 @@
      * Returns the last focal [Keyline].
      * @throws [NoSuchElementException] if there are no focal keylines.
      */
-    val lastFocal = getOrNull(lastFocalIndex)
-        ?: throw NoSuchElementException("All KeylineLists must have at least one focal keyline")
+    val lastFocal: Keyline
+        get() = getOrNull(lastFocalIndex)
+            ?: throw NoSuchElementException("All KeylineLists must have at least one focal keyline")
 
     /**
      * Returns true if the first focal item's left/top is within the visible bounds of the container
@@ -222,8 +227,14 @@
         fastForEach { keyline -> result += 31 * keyline.hashCode() }
         return result
     }
+
+    companion object {
+        val Empty = KeylineList(emptyList())
+    }
 }
 
+internal fun emptyKeylineList() = KeylineList.Empty
+
 /**
  * Returns a [KeylineList] by aligning the focal range relative to the carousel container.
  */
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
index 9caf196..f310d9f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
@@ -16,55 +16,48 @@
 
 package androidx.compose.material3.carousel
 
-import androidx.collection.IntIntMap
-import androidx.collection.emptyIntIntMap
-import androidx.collection.mutableIntIntMapOf
 import androidx.compose.foundation.gestures.snapping.SnapPosition
 import kotlin.math.max
 import kotlin.math.min
 import kotlin.math.roundToInt
 
 /**
- * Maps each item's position to the first focal keyline in a specific keyline state.
+ * Calculates the offset from the beginning of the carousel container needed to snap to the item
+ * at [itemIndex].
  *
- * The assigned keyline state guarantees the item will be at a focal position (ie. fully unmasked).
- * Keyline states are assigned in order of start state to end state for each item in order, with
- * the default keyline state assigned for extra items in the middle.
+ * This method takes into account the correct keyline list needed to allow the item to be fully
+ * visible and located at a focal position.
  */
-internal fun calculateSnapPositions(strategy: Strategy?, itemCount: Int): IntIntMap {
-    if (strategy == null || !strategy.isValid()) {
-        return emptyIntIntMap()
-    }
-    val map = mutableIntIntMapOf()
-    val defaultKeylines = strategy.defaultKeylines
-    val startKeylineSteps = strategy.startKeylineSteps
-    val endKeylineSteps = strategy.endKeylineSteps
-    val numOfFocalKeylines = defaultKeylines.lastFocalIndex - defaultKeylines.firstFocalIndex
-    val startStepsSize = startKeylineSteps.size + numOfFocalKeylines
-    val endStepsSize = endKeylineSteps.size + numOfFocalKeylines
+internal fun getSnapPositionOffset(strategy: Strategy, itemIndex: Int, itemCount: Int): Int {
+    if (!strategy.isValid) return 0
 
-    for (itemIndex in 0 until itemCount) {
-        map[itemIndex] = (defaultKeylines.firstFocal.unadjustedOffset -
-            strategy.itemMainAxisSize / 2F).roundToInt()
-        if (itemIndex < startStepsSize) {
-            var startIndex = max(0, startStepsSize - 1 - itemIndex)
-            startIndex = min(startKeylineSteps.size - 1, startIndex)
-            val startKeylines = startKeylineSteps[startIndex]
-            map[itemIndex] = (startKeylines.firstFocal.unadjustedOffset -
-                strategy.itemMainAxisSize / 2f).roundToInt()
-        }
-        if (itemCount > numOfFocalKeylines + 1 && itemIndex >= itemCount - endStepsSize) {
-            var endIndex = max(0, itemIndex - itemCount + endStepsSize)
-            endIndex = min(endKeylineSteps.size - 1, endIndex)
-            val endKeylines = endKeylineSteps[endIndex]
-            map[itemIndex] = (endKeylines.firstFocal.unadjustedOffset -
-                strategy.itemMainAxisSize / 2f).roundToInt()
-        }
+    val numOfFocalKeylines = strategy.defaultKeylines.lastFocalIndex -
+        strategy.defaultKeylines.firstFocalIndex
+    val startStepsSize = strategy.startKeylineSteps.size + numOfFocalKeylines
+    val endStepsSize = strategy.endKeylineSteps.size + numOfFocalKeylines
+
+    var offset = (strategy.defaultKeylines.firstFocal.unadjustedOffset -
+        strategy.itemMainAxisSize / 2F).roundToInt()
+
+    if (itemIndex < startStepsSize) {
+        var startIndex = max(0, startStepsSize - 1 - itemIndex)
+        startIndex = min(strategy.startKeylineSteps.size - 1, startIndex)
+        val startKeylines = strategy.startKeylineSteps[startIndex]
+        offset = (startKeylines.firstFocal.unadjustedOffset -
+            strategy.itemMainAxisSize / 2f).roundToInt()
     }
-    return map
+    if (itemCount > numOfFocalKeylines + 1 && itemIndex >= itemCount - endStepsSize) {
+        var endIndex = max(0, itemIndex - itemCount + endStepsSize)
+        endIndex = min(strategy.endKeylineSteps.size - 1, endIndex)
+        val endKeylines = strategy.endKeylineSteps[endIndex]
+        offset = (endKeylines.firstFocal.unadjustedOffset -
+            strategy.itemMainAxisSize / 2f).roundToInt()
+    }
+
+    return offset
 }
 
-internal fun KeylineSnapPosition(snapPositions: IntIntMap): SnapPosition =
+internal fun KeylineSnapPosition(pageSize: CarouselPageSize): SnapPosition =
     object : SnapPosition {
         override fun position(
             layoutSize: Int,
@@ -74,6 +67,6 @@
             itemIndex: Int,
             itemCount: Int
         ): Int {
-            return if (snapPositions.size > 0) snapPositions[itemIndex] else 0
+            return getSnapPositionOffset(pageSize.strategy, itemIndex, itemCount)
         }
     }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
index 6b47ff5..ecb6b00 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
@@ -52,9 +52,9 @@
     itemCount: Int,
     minSmallItemSize: Float = with(density) { CarouselDefaults.MinSmallItemSize.toPx() },
     maxSmallItemSize: Float = with(density) { CarouselDefaults.MaxSmallItemSize.toPx() },
-): KeylineList? {
+): KeylineList {
     if (carouselMainAxisSize == 0f || preferredItemSize == 0f) {
-        return null
+        return emptyKeylineList()
     }
 
     var smallCounts: IntArray = intArrayOf(1)
@@ -130,7 +130,7 @@
     }
 
     if (arrangement == null) {
-        return null
+        return emptyKeylineList()
     }
 
     return createLeftAlignedKeylineList(
@@ -180,9 +180,9 @@
     carouselMainAxisSize: Float,
     itemSize: Float,
     itemSpacing: Float,
-): KeylineList? {
+): KeylineList {
     if (carouselMainAxisSize == 0f || itemSize == 0f) {
-        return null
+        return emptyKeylineList()
     }
 
     val largeItemSize = min(itemSize + itemSpacing, carouselMainAxisSize)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
index 92a5bba..d548b78 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
@@ -16,12 +16,8 @@
 
 package androidx.compose.material3.carousel
 
-import androidx.annotation.VisibleForTesting
 import androidx.collection.FloatList
 import androidx.collection.mutableFloatListOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMapIndexed
@@ -31,132 +27,112 @@
 import kotlin.math.roundToInt
 
 /**
- * A class responsible for supplying carousel with a [KeylineList] that is corrected for scroll
- * offset, layout direction, and snapping behaviors.
+ * An immutable class responsible for supplying carousel with a [KeylineList] that is corrected for
+ * scroll offset, layout direction, and snapping behaviors.
  *
- * Strategy is created using a [keylineList] block that returns a default [KeylineList]. This is
- * the list of keylines that define how items should be arranged, from left-to-right,
- * to achieve the carousel's desired appearance. For example, a start-aligned large
- * item, followed by a medium and a small item for a multi-browse carousel. Or a small item,
- * a center-aligned large item, and a small item for a centered hero carousel. Strategy will
- * use the [KeylineList] returned from the [keylineList] block to then derive new scroll, and layout
- * direction-aware [KeylineList]s and supply them for use by carousel. For example, when a
- * device is running in a right-to-left layout direction, Strategy will handle reversing the default
- * [KeylineList]. Or if the default keylines use a center-aligned large item, Strategy will shift
- * the large item to the start or end of the screen when the carousel is scrolled to the start or
- * end of the list, letting all items become large without having them detach from the edges of
- * the scroll container.
- *
- * @param keylineList a function that generates default keylines for this strategy based on the
- * carousel's available space. This function will be called anytime availableSpace changes.
+ * @param defaultKeylines the keylines that define how items should be arranged in their default
+ * state
+ * @param startKeylineSteps a list of [KeylineList]s that move the focal range from its position
+ * in [defaultKeylines] to the start of the carousel container, one keyline at a time per step
+ * @param endKeylineSteps a list of [KeylineList]s that move the focal range from its position in
+ * [defaultKeylines] to the end of the carousel container, one keyline at a time per step.
+ * [endKeylineSteps] and whose value is the percentage of [endShiftDistance] that should be
+ * scrolled when the end step is used.
+ * @param availableSpace the available space in the main axis
+ * @param itemSpacing the spacing between each item
+ * @param beforeContentPadding the padding preceding the first item in the list
+ * @param afterContentPadding the padding proceeding the last item in the list
  */
-internal class Strategy(
-    private val keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?
+internal class Strategy private constructor(
+    val defaultKeylines: KeylineList,
+    val startKeylineSteps: List<KeylineList>,
+    val endKeylineSteps: List<KeylineList>,
+    val availableSpace: Float,
+    val itemSpacing: Float,
+    val beforeContentPadding: Float,
+    val afterContentPadding: Float,
 ) {
 
-    /** The keylines generated from the [keylineList] block. */
-    internal lateinit var defaultKeylines: KeylineList
     /**
-     * A list of [KeylineList]s that move the focal range from its position in [defaultKeylines]
-     * to the start of the carousel container, one keyline at a time.
+     * Creates a new [Strategy] for a keyline list and set of carousel container parameters.
+     *
+     * The [defaultKeylines] are a list of keylines that defines how items should be arranged,
+     * from left-to-right (or top-to-bottom), to achieve the carousel's desired appearance. For
+     * example, a start-aligned large item, followed by a medium and a small item for a
+     * multi-browse carousel. Or a small item, a center-aligned large item, and a small item for
+     * a centered hero carousel. This method will use the [defaultKeylines] to then derive new
+     * scroll and layout direction-aware [KeylineList]s to be used by carousel. For example, when
+     * a device is running in a right-to-left layout direction, Strategy will handle reversing
+     * the default [KeylineList]. Or if the default keylines use a center-aligned large item,
+     * Strategy will generate additional KeylineLists that handle shifting the large item to the
+     * start or end of the screen when the carousel is scrolled to the start or end of the list,
+     * letting all items become large without having them detach from the edges of the
+     * scroll container.
+     *
+     * @param defaultKeylines a default [KeylineList] that represents the arrangement
+     * of items in a left-to-right (or top-to-bottom) layout.
+     * @param availableSpace the size of the carousel container in scrolling axis
+     * @param beforeContentPadding the padding to add before the list content
+     * @param afterContentPadding the padding to add after the list content
      */
-    internal lateinit var startKeylineSteps: List<KeylineList>
-    /**
-     * A list of [KeylineList]s that move the focal range from its position in [defaultKeylines]
-     * to the end of the carousel container, one keyline at a time.
-     */
-    internal lateinit var endKeylineSteps: List<KeylineList>
+    constructor(
+        defaultKeylines: KeylineList,
+        availableSpace: Float,
+        itemSpacing: Float,
+        beforeContentPadding: Float,
+        afterContentPadding: Float
+    ) : this(
+        defaultKeylines = defaultKeylines,
+        startKeylineSteps = getStartKeylineSteps(
+            defaultKeylines,
+            availableSpace,
+            itemSpacing,
+            beforeContentPadding
+        ),
+        endKeylineSteps = getEndKeylineSteps(
+            defaultKeylines,
+            availableSpace,
+            itemSpacing,
+            afterContentPadding
+        ),
+        availableSpace = availableSpace,
+        itemSpacing = itemSpacing,
+        beforeContentPadding = beforeContentPadding,
+        afterContentPadding = afterContentPadding,
+    )
+
     /** The scroll distance needed to move through all steps in [startKeylineSteps]. */
-    private var startShiftDistance: Float = 0f
+    private val startShiftDistance = getStartShiftDistance(startKeylineSteps, beforeContentPadding)
     /** The scroll distance needed to move through all steps in [endKeylineSteps]. */
-    private var endShiftDistance: Float = 0f
+    private val endShiftDistance = getEndShiftDistance(endKeylineSteps, afterContentPadding)
     /**
      * A list of floats whose index aligns with a [KeylineList] from [startKeylineSteps] and
      * whose value is the percentage of [startShiftDistance] that should be scrolled when the
      * start step is used.
      */
-    private lateinit var startShiftPoints: FloatList
+    private val startShiftPoints = getStepInterpolationPoints(
+        startShiftDistance,
+        startKeylineSteps,
+        true
+    )
     /**
      * A list of floats whose index aligns with a [KeylineList] from [endKeylineSteps] and
      * whose value is the percentage of [endShiftDistance] that should be scrolled when the
      * end step is used.
      */
-    private lateinit var endShiftPoints: FloatList
+    private val endShiftPoints = getStepInterpolationPoints(
+        endShiftDistance,
+        endKeylineSteps,
+        false
+    )
 
-    /** The available space in the main axis used in the most recent call to [apply]. */
-    internal var availableSpace: Float = 0f
-    /** The spacing between each item. */
-    internal var itemSpacing: Float = 0f
-    internal var beforeContentPadding: Float = 0f
-    internal var afterContentPadding: Float = 0f
     /** The size of items when in focus and fully unmasked. */
-    internal var itemMainAxisSize by mutableFloatStateOf(0f)
+    val itemMainAxisSize: Float
+        get() = defaultKeylines.firstFocal.size
 
-    /**
-     * Whether this strategy holds a valid set of keylines that are ready for use.
-     *
-     * This is true after [apply] has been called and the [keylineList] block has returned a
-     * non-null [KeylineList].
-     */
-    fun isValid() = itemMainAxisSize > 0f
-
-    /**
-     * Updates this [Strategy] based on carousel's main axis available space.
-     *
-     * This method must be called before a strategy can be used by carousel.
-     *
-     * @param availableSpace the size of the carousel container in scrolling axis
-     * @param beforeContentPadding the padding to add before the list content
-     * @param afterContentPadding the padding to add after the list content
-     */
-    internal fun apply(
-        availableSpace: Float,
-        itemSpacing: Float,
-        beforeContentPadding: Float,
-        afterContentPadding: Float
-    ): Strategy {
-        // Skip computing new keylines and updating this strategy if
-        // available space has not changed.
-        if (this.availableSpace == availableSpace && this.itemSpacing == itemSpacing) {
-            return this
-        }
-
-        val keylineList = keylineList.invoke(availableSpace, itemSpacing) ?: return this
-        val startKeylineSteps =
-            getStartKeylineSteps(keylineList, availableSpace, itemSpacing, beforeContentPadding)
-        val endKeylineSteps =
-            getEndKeylineSteps(keylineList, availableSpace, itemSpacing, afterContentPadding)
-
-        // TODO: Update this to use the first/last focal keylines to calculate shift?
-        val startShiftDistance = max(startKeylineSteps.last().first().unadjustedOffset -
-            keylineList.first().unadjustedOffset, beforeContentPadding)
-        val endShiftDistance = max(keylineList.last().unadjustedOffset -
-            endKeylineSteps.last().last().unadjustedOffset, afterContentPadding)
-
-        this.defaultKeylines = keylineList
-        this.defaultKeylines = keylineList
-        this.startKeylineSteps = startKeylineSteps
-        this.endKeylineSteps = endKeylineSteps
-        this.startShiftDistance = startShiftDistance
-        this.endShiftDistance = endShiftDistance
-        this.startShiftPoints = getStepInterpolationPoints(
-            startShiftDistance,
-            startKeylineSteps,
-            true
-        )
-        this.endShiftPoints = getStepInterpolationPoints(
-            endShiftDistance,
-            endKeylineSteps,
-            false
-        )
-        this.availableSpace = availableSpace
-        this.itemSpacing = itemSpacing
-        this.beforeContentPadding = beforeContentPadding
-        this.afterContentPadding = afterContentPadding
-        this.itemMainAxisSize = defaultKeylines.firstFocal.size
-
-        return this
-    }
+    /** True if this strategy contains a valid arrangement of keylines for a valid container */
+    val isValid: Boolean =
+        defaultKeylines.isNotEmpty() && availableSpace != 0f && itemMainAxisSize != 0f
 
     /**
      * Returns the [KeylineList] that should be used for the current [scrollOffset].
@@ -225,23 +201,14 @@
         )
     }
 
-    @VisibleForTesting
-    internal fun getEndKeylines(): KeylineList {
-        return endKeylineSteps.last()
-    }
-
-    @VisibleForTesting
-    internal fun getStartKeylines(): KeylineList {
-        return startKeylineSteps.last()
-    }
-
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is Strategy) return false
-        // If neither strategy is valid, they should be considered equal
-        if (!isValid() && !other.isValid()) return true
 
-        if (isValid() != other.isValid()) return false
+        // If neither strategy is valid, they should be considered equal
+        if (!isValid && !other.isValid) return true
+
+        if (isValid != other.isValid) return false
         if (availableSpace != other.availableSpace) return false
         if (itemSpacing != other.itemSpacing) return false
         if (beforeContentPadding != other.beforeContentPadding) return false
@@ -259,9 +226,9 @@
     }
 
     override fun hashCode(): Int {
-        if (!isValid()) return isValid().hashCode()
+        if (!isValid) return isValid.hashCode()
 
-        var result = isValid().hashCode()
+        var result = isValid.hashCode()
         result = 31 * result + availableSpace.hashCode()
         result = 31 * result + itemSpacing.hashCode()
         result = 31 * result + beforeContentPadding.hashCode()
@@ -276,352 +243,387 @@
     }
 
     companion object {
+        val Empty = Strategy(
+            defaultKeylines = emptyKeylineList(),
+            startKeylineSteps = emptyList(),
+            endKeylineSteps = emptyList(),
+            availableSpace = 0f,
+            itemSpacing = 0f,
+            beforeContentPadding = 0f,
+            afterContentPadding = 0f,
+        )
+    }
+}
 
-        /**
-         * Generates discreet steps which move the focal range from its original position until
-         * it reaches the start of the carousel container.
-         *
-         * Each step can only move the focal range by one keyline at a time to ensure every
-         * item in the list passes through the focal range. Each step removes the keyline at the
-         * start of the container and re-inserts it after the focal range in an order that retains
-         * visual balance. This is repeated until the first focal keyline is at the start of the
-         * container. Re-inserting keylines after the focal range in a balanced way is done by
-         * looking at the size of they keyline next to the keyline that is being re-positioned
-         * and finding a match on the other side of the focal range.
-         *
-         * The first state in the returned list is always the default [KeylineList] while
-         * the last state will be the start state or the state that has the focal range at the
-         * beginning of the carousel.
-         */
-        private fun getStartKeylineSteps(
-            defaultKeylines: KeylineList,
-            carouselMainAxisSize: Float,
-            itemSpacing: Float,
-            beforeContentPadding: Float
-        ): List<KeylineList> {
-            val steps: MutableList<KeylineList> = mutableListOf()
-            steps.add(defaultKeylines)
+/**
+ * Returns the total scroll offset needed to move through the entire list of [startKeylineSteps].
+ */
+private fun getStartShiftDistance(
+    startKeylineSteps: List<KeylineList>,
+    beforeContentPadding: Float
+): Float {
+    if (startKeylineSteps.isEmpty()) return 0f
+    return max(startKeylineSteps.last().first().unadjustedOffset -
+        startKeylineSteps.first().first().unadjustedOffset, beforeContentPadding)
+}
+/**
+ * Returns the total scroll offset needed to move through the entire list of [endKeylineSteps].
+ */
+private fun getEndShiftDistance(
+    endKeylineSteps: List<KeylineList>,
+    afterContentPadding: Float
+): Float {
+    if (endKeylineSteps.isEmpty()) return 0f
+    return max(endKeylineSteps.first().last().unadjustedOffset -
+        endKeylineSteps.last().last().unadjustedOffset, afterContentPadding)
+}
 
-            if (defaultKeylines.isFirstFocalItemAtStartOfContainer()) {
-                if (beforeContentPadding != 0f) {
-                    steps.add(
-                        createShiftedKeylineListForContentPadding(
-                            defaultKeylines,
-                            carouselMainAxisSize,
-                            itemSpacing,
-                            beforeContentPadding,
-                            defaultKeylines.firstFocal,
-                            defaultKeylines.firstFocalIndex
-                        )
-                    )
-                }
-                return steps
-            }
+/**
+ * Generates discreet steps which move the focal range from its original position until
+ * it reaches the start of the carousel container.
+ *
+ * Each step can only move the focal range by one keyline at a time to ensure every
+ * item in the list passes through the focal range. Each step removes the keyline at the
+ * start of the container and re-inserts it after the focal range in an order that retains
+ * visual balance. This is repeated until the first focal keyline is at the start of the
+ * container. Re-inserting keylines after the focal range in a balanced way is done by
+ * looking at the size of they keyline next to the keyline that is being re-positioned
+ * and finding a match on the other side of the focal range.
+ *
+ * The first state in the returned list is always the default [KeylineList] while
+ * the last state will be the start state or the state that has the focal range at the
+ * beginning of the carousel.
+ */
+private fun getStartKeylineSteps(
+    defaultKeylines: KeylineList,
+    carouselMainAxisSize: Float,
+    itemSpacing: Float,
+    beforeContentPadding: Float
+): List<KeylineList> {
+    if (defaultKeylines.isEmpty()) return emptyList()
 
-            val startIndex = defaultKeylines.firstNonAnchorIndex
-            val endIndex = defaultKeylines.firstFocalIndex
-            val numberOfSteps = endIndex - startIndex
+    val steps: MutableList<KeylineList> = mutableListOf()
+    steps.add(defaultKeylines)
 
-            // If there are no steps but we need to account for a cutoff, create a
-            // list of keylines shifted for the cutoff.
-            if (numberOfSteps <= 0 && defaultKeylines.firstFocal.cutoff > 0) {
-                steps.add(
-                    moveKeylineAndCreateShiftedKeylineList(
-                        from = defaultKeylines,
-                        srcIndex = 0,
-                        dstIndex = 0,
-                        carouselMainAxisSize = carouselMainAxisSize,
-                        itemSpacing = itemSpacing
-                    )
-                )
-                return steps
-            }
-
-            var i = 0
-            while (i < numberOfSteps) {
-                val prevStep = steps.last()
-                val originalItemIndex = startIndex + i
-                var dstIndex = defaultKeylines.lastIndex
-                if (originalItemIndex > 0) {
-                    val originalNeighborBeforeSize = defaultKeylines[originalItemIndex - 1].size
-                    dstIndex = prevStep.firstIndexAfterFocalRangeWithSize(
-                        originalNeighborBeforeSize
-                    ) - 1
-                }
-
-                steps.add(
-                    moveKeylineAndCreateShiftedKeylineList(
-                        from = prevStep,
-                        srcIndex = defaultKeylines.firstNonAnchorIndex,
-                        dstIndex = dstIndex,
-                        carouselMainAxisSize = carouselMainAxisSize,
-                        itemSpacing = itemSpacing
-                    )
-                )
-                i++
-            }
-
-            if (beforeContentPadding != 0f) {
-                steps[steps.lastIndex] = createShiftedKeylineListForContentPadding(
-                    steps.last(),
+    if (defaultKeylines.isFirstFocalItemAtStartOfContainer()) {
+        if (beforeContentPadding != 0f) {
+            steps.add(
+                createShiftedKeylineListForContentPadding(
+                    defaultKeylines,
                     carouselMainAxisSize,
                     itemSpacing,
                     beforeContentPadding,
-                    steps.last().firstFocal,
-                    steps.last().firstFocalIndex
+                    defaultKeylines.firstFocal,
+                    defaultKeylines.firstFocalIndex
                 )
-            }
-
-            return steps
-        }
-
-        /**
-         * Generates discreet steps which move the focal range from its original position until
-         * it reaches the end of the carousel container.
-         *
-         * Each step can only move the focal range by one keyline at a time to ensure every
-         * item in the list passes through the focal range. Each step removes the keyline at the
-         * end of the container and re-inserts it before the focal range in an order that retains
-         * visual balance. This is repeated until the last focal keyline is at the start of the
-         * container. Re-inserting keylines before the focal range in a balanced way is done by
-         * looking at the size of they keyline next to the keyline that is being re-positioned
-         * and finding a match on the other side of the focal range.
-         *
-         * The first state in the returned list is always the default [KeylineList] while
-         * the last state will be the end state or the state that has the focal range at the
-         * end of the carousel.
-         */
-        private fun getEndKeylineSteps(
-            defaultKeylines: KeylineList,
-            carouselMainAxisSize: Float,
-            itemSpacing: Float,
-            afterContentPadding: Float
-        ): List<KeylineList> {
-            val steps: MutableList<KeylineList> = mutableListOf()
-            steps.add(defaultKeylines)
-
-            if (defaultKeylines.isLastFocalItemAtEndOfContainer(carouselMainAxisSize)) {
-                if (afterContentPadding != 0f) {
-                    steps.add(createShiftedKeylineListForContentPadding(
-                        defaultKeylines,
-                        carouselMainAxisSize,
-                        itemSpacing,
-                        -afterContentPadding,
-                        defaultKeylines.lastFocal,
-                        defaultKeylines.lastFocalIndex
-                    ))
-                }
-                return steps
-            }
-
-            val startIndex = defaultKeylines.lastFocalIndex
-            val endIndex = defaultKeylines.lastNonAnchorIndex
-            val numberOfSteps = endIndex - startIndex
-
-            // If there are no steps but we need to account for a cutoff, create a
-            // list of keylines shifted for the cutoff.
-            if (numberOfSteps <= 0 && defaultKeylines.lastFocal.cutoff > 0) {
-                steps.add(
-                    moveKeylineAndCreateShiftedKeylineList(
-                        from = defaultKeylines,
-                        srcIndex = 0,
-                        dstIndex = 0,
-                        carouselMainAxisSize = carouselMainAxisSize,
-                        itemSpacing = itemSpacing
-                    )
-                )
-                return steps
-            }
-
-            var i = 0
-            while (i < numberOfSteps) {
-                val prevStep = steps.last()
-                val originalItemIndex = endIndex - i
-                var dstIndex = 0
-
-                if (originalItemIndex < defaultKeylines.lastIndex) {
-                    val originalNeighborAfterSize = defaultKeylines[originalItemIndex + 1].size
-                    dstIndex = prevStep.lastIndexBeforeFocalRangeWithSize(
-                        originalNeighborAfterSize
-                    ) + 1
-                }
-
-                val keylines = moveKeylineAndCreateShiftedKeylineList(
-                    from = prevStep,
-                    srcIndex = defaultKeylines.lastNonAnchorIndex,
-                    dstIndex = dstIndex,
-                    carouselMainAxisSize = carouselMainAxisSize,
-                    itemSpacing = itemSpacing
-                )
-                steps.add(keylines)
-                i++
-            }
-
-            if (afterContentPadding != 0f) {
-                steps[steps.lastIndex] = createShiftedKeylineListForContentPadding(
-                    steps.last(),
-                    carouselMainAxisSize,
-                    itemSpacing,
-                    -afterContentPadding,
-                    steps.last().lastFocal,
-                    steps.last().lastFocalIndex
-                )
-            }
-
-            return steps
-        }
-
-        /**
-         * Returns a new [KeylineList] identical to [from] but with each keyline's offset shifted
-         * by [contentPadding].
-         */
-        private fun createShiftedKeylineListForContentPadding(
-            from: KeylineList,
-            carouselMainAxisSize: Float,
-            itemSpacing: Float,
-            contentPadding: Float,
-            pivot: Keyline,
-            pivotIndex: Int
-        ): KeylineList {
-            val numberOfNonAnchorKeylines = from.fastFilter { !it.isAnchor }.count()
-            val sizeReduction = contentPadding / numberOfNonAnchorKeylines
-            // Let keylineListOf create a new keyline list with offsets adjusted for each item's
-            // reduction in size
-            val newKeylines = keylineListOf(
-                carouselMainAxisSize = carouselMainAxisSize,
-                itemSpacing = itemSpacing,
-                pivotIndex = pivotIndex,
-                pivotOffset = pivot.offset - (sizeReduction / 2f) + contentPadding
-            ) {
-                from.fastForEach { k -> add(k.size - abs(sizeReduction), k.isAnchor) }
-            }
-
-            // Then reset each item's unadjusted offset back to their original value from the
-            // incoming keyline list. This is necessary because Pager will still be laying out items
-            // end-to-end with the original page size and not the new reduced size.
-            return KeylineList(
-                newKeylines.fastMapIndexed { i, k ->
-                    k.copy(
-                        unadjustedOffset = from[i].unadjustedOffset
-                    )
-                }
             )
         }
-
-        /**
-         * Returns a new [KeylineList] where the keyline at [srcIndex] is moved to [dstIndex] and
-         * with updated pivot and offsets that reflect any change in focal shift.
-         */
-        private fun moveKeylineAndCreateShiftedKeylineList(
-            from: KeylineList,
-            srcIndex: Int,
-            dstIndex: Int,
-            carouselMainAxisSize: Float,
-            itemSpacing: Float
-        ): KeylineList {
-            // -1 if the pivot is shifting left/top, 1 if shifting right/bottom
-            val pivotDir = if (srcIndex > dstIndex) 1 else -1
-            val pivotDelta = (from[srcIndex].size - from[srcIndex].cutoff + itemSpacing) * pivotDir
-            val newPivotIndex = from.pivotIndex + pivotDir
-            val newPivotOffset = from.pivot.offset + pivotDelta
-            return keylineListOf(carouselMainAxisSize, itemSpacing, newPivotIndex, newPivotOffset) {
-                from.toMutableList()
-                    .move(srcIndex, dstIndex)
-                    .fastForEach { k -> add(k.size, k.isAnchor) }
-            }
-        }
-
-        /**
-         * Creates and returns a list of float values containing points between 0 and 1 that
-         * represent interpolation values for when the [KeylineList] at the corresponding index in
-         * [steps] should be visible.
-         *
-         * For example, if [steps] has a size of 4, this method will return an array of 4 float
-         * values that could look like [0, .33, .66, 1]. When interpolating through a list of
-         * [KeylineList]s, an interpolation value will be between 0-1. This interpolation will be
-         * used to find the range it falls within from this method's returned value. If
-         * interpolation is .25, that would fall between the 0 and .33, the 0th and 1st indices
-         * of the float array. Meaning the 0th and 1st items from [steps] should be the current
-         * [KeylineList]s being interpolated. This is an example with equally distributed values
-         * but these values will typically be unequally distributed since their size depends on
-         * the distance keylines shift between each step.
-         *
-         * @see [lerp] for more details on how interpolation points are used
-         * @see [getKeylineListForScrollOffset] for more details on how interpolation points
-         * are used
-         *
-         * @param totalShiftDistance the total distance keylines will shift between the first and
-         * last [KeylineList] of [steps]
-         * @param steps the steps to find interpolation points for
-         * @param isShiftingLeft true if this method should find interpolation points for shifting
-         * keylines to the left/top of a carousel, false if this method should find interpolation
-         * points for shifting keylines to the right/bottom of a carousel
-         * @return a list of floats, equal in size to [steps] that contains points between 0-1
-         * that align with when a [KeylineList] from [steps should be shown for a 0-1
-         * interpolation value
-         */
-        private fun getStepInterpolationPoints(
-            totalShiftDistance: Float,
-            steps: List<KeylineList>,
-            isShiftingLeft: Boolean
-        ): FloatList {
-            val points = mutableFloatListOf(0f)
-            if (totalShiftDistance == 0f) {
-                return points
-            }
-
-            (1 until steps.size).map { i ->
-                val prevKeylines = steps[i - 1]
-                val currKeylines = steps[i]
-                val distanceShifted = if (isShiftingLeft) {
-                    currKeylines.first().unadjustedOffset - prevKeylines.first().unadjustedOffset
-                } else {
-                    prevKeylines.last().unadjustedOffset - currKeylines.last().unadjustedOffset
-                }
-                val stepPercentage = distanceShifted / totalShiftDistance
-                val point = if (i == steps.lastIndex) 1f else points[i - 1] + stepPercentage
-                points.add(point)
-            }
-            return points
-        }
-
-        private data class ShiftPointRange(
-            val fromStepIndex: Int,
-            val toStepIndex: Int,
-            val steppedInterpolation: Float
-        )
-
-        private fun getShiftPointRange(
-            stepsCount: Int,
-            shiftPoint: FloatList,
-            interpolation: Float
-        ): ShiftPointRange {
-            var lowerBounds = shiftPoint[0]
-            (1 until stepsCount).forEach { i ->
-                val upperBounds = shiftPoint[i]
-                if (interpolation <= upperBounds) {
-                    return ShiftPointRange(
-                        fromStepIndex = i - 1,
-                        toStepIndex = i,
-                        steppedInterpolation = lerp(0f, 1f, lowerBounds, upperBounds, interpolation)
-                    )
-                }
-                lowerBounds = upperBounds
-            }
-            return ShiftPointRange(
-                fromStepIndex = 0,
-                toStepIndex = 0,
-                steppedInterpolation = 0f
-            )
-        }
-
-        private fun MutableList<Keyline>.move(srcIndex: Int, dstIndex: Int): MutableList<Keyline> {
-            val keyline = get(srcIndex)
-            removeAt(srcIndex)
-            add(dstIndex, keyline)
-            return this
-        }
+        return steps
     }
+
+    val startIndex = defaultKeylines.firstNonAnchorIndex
+    val endIndex = defaultKeylines.firstFocalIndex
+    val numberOfSteps = endIndex - startIndex
+
+    // If there are no steps but we need to account for a cutoff, create a
+    // list of keylines shifted for the cutoff.
+    if (numberOfSteps <= 0 && defaultKeylines.firstFocal.cutoff > 0) {
+        steps.add(
+            moveKeylineAndCreateShiftedKeylineList(
+                from = defaultKeylines,
+                srcIndex = 0,
+                dstIndex = 0,
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = itemSpacing
+            )
+        )
+        return steps
+    }
+
+    var i = 0
+    while (i < numberOfSteps) {
+        val prevStep = steps.last()
+        val originalItemIndex = startIndex + i
+        var dstIndex = defaultKeylines.lastIndex
+        if (originalItemIndex > 0) {
+            val originalNeighborBeforeSize = defaultKeylines[originalItemIndex - 1].size
+            dstIndex = prevStep.firstIndexAfterFocalRangeWithSize(
+                originalNeighborBeforeSize
+            ) - 1
+        }
+
+        steps.add(
+            moveKeylineAndCreateShiftedKeylineList(
+                from = prevStep,
+                srcIndex = defaultKeylines.firstNonAnchorIndex,
+                dstIndex = dstIndex,
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = itemSpacing
+            )
+        )
+        i++
+    }
+
+    if (beforeContentPadding != 0f) {
+        steps[steps.lastIndex] = createShiftedKeylineListForContentPadding(
+            steps.last(),
+            carouselMainAxisSize,
+            itemSpacing,
+            beforeContentPadding,
+            steps.last().firstFocal,
+            steps.last().firstFocalIndex
+        )
+    }
+
+    return steps
+}
+
+/**
+ * Generates discreet steps which move the focal range from its original position until
+ * it reaches the end of the carousel container.
+ *
+ * Each step can only move the focal range by one keyline at a time to ensure every
+ * item in the list passes through the focal range. Each step removes the keyline at the
+ * end of the container and re-inserts it before the focal range in an order that retains
+ * visual balance. This is repeated until the last focal keyline is at the start of the
+ * container. Re-inserting keylines before the focal range in a balanced way is done by
+ * looking at the size of they keyline next to the keyline that is being re-positioned
+ * and finding a match on the other side of the focal range.
+ *
+ * The first state in the returned list is always the default [KeylineList] while
+ * the last state will be the end state or the state that has the focal range at the
+ * end of the carousel.
+ */
+private fun getEndKeylineSteps(
+    defaultKeylines: KeylineList,
+    carouselMainAxisSize: Float,
+    itemSpacing: Float,
+    afterContentPadding: Float
+): List<KeylineList> {
+    if (defaultKeylines.isEmpty()) return emptyList()
+    val steps: MutableList<KeylineList> = mutableListOf()
+    steps.add(defaultKeylines)
+
+    if (defaultKeylines.isLastFocalItemAtEndOfContainer(carouselMainAxisSize)) {
+        if (afterContentPadding != 0f) {
+            steps.add(createShiftedKeylineListForContentPadding(
+                defaultKeylines,
+                carouselMainAxisSize,
+                itemSpacing,
+                -afterContentPadding,
+                defaultKeylines.lastFocal,
+                defaultKeylines.lastFocalIndex
+            ))
+        }
+        return steps
+    }
+
+    val startIndex = defaultKeylines.lastFocalIndex
+    val endIndex = defaultKeylines.lastNonAnchorIndex
+    val numberOfSteps = endIndex - startIndex
+
+    // If there are no steps but we need to account for a cutoff, create a
+    // list of keylines shifted for the cutoff.
+    if (numberOfSteps <= 0 && defaultKeylines.lastFocal.cutoff > 0) {
+        steps.add(
+            moveKeylineAndCreateShiftedKeylineList(
+                from = defaultKeylines,
+                srcIndex = 0,
+                dstIndex = 0,
+                carouselMainAxisSize = carouselMainAxisSize,
+                itemSpacing = itemSpacing
+            )
+        )
+        return steps
+    }
+
+    var i = 0
+    while (i < numberOfSteps) {
+        val prevStep = steps.last()
+        val originalItemIndex = endIndex - i
+        var dstIndex = 0
+
+        if (originalItemIndex < defaultKeylines.lastIndex) {
+            val originalNeighborAfterSize = defaultKeylines[originalItemIndex + 1].size
+            dstIndex = prevStep.lastIndexBeforeFocalRangeWithSize(
+                originalNeighborAfterSize
+            ) + 1
+        }
+
+        val keylines = moveKeylineAndCreateShiftedKeylineList(
+            from = prevStep,
+            srcIndex = defaultKeylines.lastNonAnchorIndex,
+            dstIndex = dstIndex,
+            carouselMainAxisSize = carouselMainAxisSize,
+            itemSpacing = itemSpacing
+        )
+        steps.add(keylines)
+        i++
+    }
+
+    if (afterContentPadding != 0f) {
+        steps[steps.lastIndex] = createShiftedKeylineListForContentPadding(
+            steps.last(),
+            carouselMainAxisSize,
+            itemSpacing,
+            -afterContentPadding,
+            steps.last().lastFocal,
+            steps.last().lastFocalIndex
+        )
+    }
+
+    return steps
+}
+
+/**
+ * Returns a new [KeylineList] identical to [from] but with each keyline's offset shifted
+ * by [contentPadding].
+ */
+private fun createShiftedKeylineListForContentPadding(
+    from: KeylineList,
+    carouselMainAxisSize: Float,
+    itemSpacing: Float,
+    contentPadding: Float,
+    pivot: Keyline,
+    pivotIndex: Int
+): KeylineList {
+    val numberOfNonAnchorKeylines = from.fastFilter { !it.isAnchor }.count()
+    val sizeReduction = contentPadding / numberOfNonAnchorKeylines
+    // Let keylineListOf create a new keyline list with offsets adjusted for each item's
+    // reduction in size
+    val newKeylines = keylineListOf(
+        carouselMainAxisSize = carouselMainAxisSize,
+        itemSpacing = itemSpacing,
+        pivotIndex = pivotIndex,
+        pivotOffset = pivot.offset - (sizeReduction / 2f) + contentPadding
+    ) {
+        from.fastForEach { k -> add(k.size - abs(sizeReduction), k.isAnchor) }
+    }
+
+    // Then reset each item's unadjusted offset back to their original value from the
+    // incoming keyline list. This is necessary because Pager will still be laying out items
+    // end-to-end with the original page size and not the new reduced size.
+    return KeylineList(
+        newKeylines.fastMapIndexed { i, k ->
+            k.copy(
+                unadjustedOffset = from[i].unadjustedOffset
+            )
+        }
+    )
+}
+
+/**
+ * Returns a new [KeylineList] where the keyline at [srcIndex] is moved to [dstIndex] and
+ * with updated pivot and offsets that reflect any change in focal shift.
+ */
+private fun moveKeylineAndCreateShiftedKeylineList(
+    from: KeylineList,
+    srcIndex: Int,
+    dstIndex: Int,
+    carouselMainAxisSize: Float,
+    itemSpacing: Float
+): KeylineList {
+    // -1 if the pivot is shifting left/top, 1 if shifting right/bottom
+    val pivotDir = if (srcIndex > dstIndex) 1 else -1
+    val pivotDelta = (from[srcIndex].size - from[srcIndex].cutoff + itemSpacing) * pivotDir
+    val newPivotIndex = from.pivotIndex + pivotDir
+    val newPivotOffset = from.pivot.offset + pivotDelta
+    return keylineListOf(carouselMainAxisSize, itemSpacing, newPivotIndex, newPivotOffset) {
+        from.toMutableList()
+            .move(srcIndex, dstIndex)
+            .fastForEach { k -> add(k.size, k.isAnchor) }
+    }
+}
+
+/**
+ * Creates and returns a list of float values containing points between 0 and 1 that
+ * represent interpolation values for when the [KeylineList] at the corresponding index in
+ * [steps] should be visible.
+ *
+ * For example, if [steps] has a size of 4, this method will return an array of 4 float
+ * values that could look like [0, .33, .66, 1]. When interpolating through a list of
+ * [KeylineList]s, an interpolation value will be between 0-1. This interpolation will be
+ * used to find the range it falls within from this method's returned value. If
+ * interpolation is .25, that would fall between the 0 and .33, the 0th and 1st indices
+ * of the float array. Meaning the 0th and 1st items from [steps] should be the current
+ * [KeylineList]s being interpolated. This is an example with equally distributed values
+ * but these values will typically be unequally distributed since their size depends on
+ * the distance keylines shift between each step.
+ *
+ * @see [lerp] for more details on how interpolation points are used
+ * @see [Strategy.getKeylineListForScrollOffset] for more details on how interpolation points
+ * are used
+ *
+ * @param totalShiftDistance the total distance keylines will shift between the first and
+ * last [KeylineList] of [steps]
+ * @param steps the steps to find interpolation points for
+ * @param isShiftingLeft true if this method should find interpolation points for shifting
+ * keylines to the left/top of a carousel, false if this method should find interpolation
+ * points for shifting keylines to the right/bottom of a carousel
+ * @return a list of floats, equal in size to [steps] that contains points between 0-1
+ * that align with when a [KeylineList] from [steps should be shown for a 0-1
+ * interpolation value
+ */
+private fun getStepInterpolationPoints(
+    totalShiftDistance: Float,
+    steps: List<KeylineList>,
+    isShiftingLeft: Boolean
+): FloatList {
+    val points = mutableFloatListOf(0f)
+    if (totalShiftDistance == 0f || steps.isEmpty()) {
+        return points
+    }
+
+    (1 until steps.size).map { i ->
+        val prevKeylines = steps[i - 1]
+        val currKeylines = steps[i]
+        val distanceShifted = if (isShiftingLeft) {
+            currKeylines.first().unadjustedOffset - prevKeylines.first().unadjustedOffset
+        } else {
+            prevKeylines.last().unadjustedOffset - currKeylines.last().unadjustedOffset
+        }
+        val stepPercentage = distanceShifted / totalShiftDistance
+        val point = if (i == steps.lastIndex) 1f else points[i - 1] + stepPercentage
+        points.add(point)
+    }
+    return points
+}
+
+private data class ShiftPointRange(
+    val fromStepIndex: Int,
+    val toStepIndex: Int,
+    val steppedInterpolation: Float
+)
+
+private fun getShiftPointRange(
+    stepsCount: Int,
+    shiftPoint: FloatList,
+    interpolation: Float
+): ShiftPointRange {
+    var lowerBounds = shiftPoint[0]
+    (1 until stepsCount).forEach { i ->
+        val upperBounds = shiftPoint[i]
+        if (interpolation <= upperBounds) {
+            return ShiftPointRange(
+                fromStepIndex = i - 1,
+                toStepIndex = i,
+                steppedInterpolation = lerp(0f, 1f, lowerBounds, upperBounds, interpolation)
+            )
+        }
+        lowerBounds = upperBounds
+    }
+    return ShiftPointRange(
+        fromStepIndex = 0,
+        toStepIndex = 0,
+        steppedInterpolation = 0f
+    )
+}
+
+private fun MutableList<Keyline>.move(srcIndex: Int, dstIndex: Int): MutableList<Keyline> {
+    val keyline = get(srcIndex)
+    removeAt(srcIndex)
+    add(dstIndex, keyline)
+    return this
 }
 
 private fun lerp(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index f147076..cf714ea 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -820,7 +820,7 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
 
-        other as DraggableAnchorsElement<*>
+        if (other !is DraggableAnchorsElement<*>) return false
 
         if (state != other.state) return false
         if (anchors != other.anchors) return false
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index d2c2079..bad1aef 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
      * IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
      * `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
      */
-    const val version: Int = 12500
+    const val version: Int = 12600
 }
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
index b51b5f1..2f729b2 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
@@ -37,7 +37,6 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
@@ -279,7 +278,6 @@
         }
     }
 
-    @OptIn(ExperimentalComposeUiApi::class)
     private fun Modifier.animatePlacement(
         targetOffset: MutableState<Offset>,
         alignment: () -> Alignment
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyDownTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyDownTest.kt
index 6d6d75a..58471ca 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyDownTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyDownTest.kt
@@ -17,9 +17,7 @@
 package androidx.compose.ui.test.injectionscope.key
 
 import androidx.compose.testutils.expectError
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.KeyInjectionScope
 import androidx.compose.ui.test.injectionscope.key.Common.assertTyped
 import androidx.compose.ui.test.injectionscope.key.Common.performKeyInput
@@ -38,7 +36,6 @@
  * Tests if [KeyInjectionScope.keyDown] works
  */
 @MediumTest
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
 class KeyDownTest {
 
     @get:Rule
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyPressTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyPressTest.kt
index 913e47da8..634bc27 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyPressTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyPressTest.kt
@@ -16,9 +16,7 @@
 
 package androidx.compose.ui.test.injectionscope.key
 
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.KeyInjectionScope
 import androidx.compose.ui.test.injectionscope.key.Common.assertTyped
 import androidx.compose.ui.test.injectionscope.key.Common.performKeyInput
@@ -38,7 +36,6 @@
  * Tests if [KeyInjectionScope.pressKey] works.
  */
 @LargeTest
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
 class KeyPressTest {
 
     @get:Rule
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyUpTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyUpTest.kt
index 4ded126..e83f1c5 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyUpTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/KeyUpTest.kt
@@ -17,9 +17,7 @@
 package androidx.compose.ui.test.injectionscope.key
 
 import androidx.compose.testutils.expectError
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.KeyInjectionScope
 import androidx.compose.ui.test.injectionscope.key.Common.assertTyped
 import androidx.compose.ui.test.injectionscope.key.Common.performKeyInput
@@ -37,7 +35,6 @@
  * Tests if [KeyInjectionScope.keyUp] works
  */
 @MediumTest
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
 class KeyUpTest {
 
     @get:Rule
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/LockKeysTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/LockKeysTest.kt
index 5cad927..028adab 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/LockKeysTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/key/LockKeysTest.kt
@@ -16,9 +16,7 @@
 
 package androidx.compose.ui.test.injectionscope.key
 
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.KeyInjectionScope
 import androidx.compose.ui.test.injectionscope.key.Common.assertTyped
 import androidx.compose.ui.test.injectionscope.key.Common.performKeyInput
@@ -41,7 +39,6 @@
  * Tests if the lock key methods in [KeyInjectionScope] like [KeyInjectionScope.isCapsLockOn] work.
  */
 @MediumTest
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
 class LockKeysTest {
 
     @get:Rule
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeDirectionTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeDirectionTest.kt
index 3a09756..509b72f 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeDirectionTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeDirectionTest.kt
@@ -23,7 +23,6 @@
 import androidx.compose.testutils.expectError
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.TouchInjectionScope
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -132,7 +131,6 @@
     @Test
     fun swipeUp_withParameters() {
         rule.setContent { Ui(Alignment.TopStart) }
-        @OptIn(ExperimentalTestApi::class)
         rule.onNodeWithTag(tag).performTouchInput { swipeUp(endY = centerY) }
         rule.runOnIdle {
             recorder.run {
@@ -148,7 +146,6 @@
     @Test
     fun swipeDown_withParameters() {
         rule.setContent { Ui(Alignment.TopEnd) }
-        @OptIn(ExperimentalTestApi::class)
         rule.onNodeWithTag(tag).performTouchInput { swipeDown(endY = centerY) }
         rule.runOnIdle {
             recorder.run {
@@ -164,7 +161,6 @@
     @Test
     fun swipeLeft_withParameters() {
         rule.setContent { Ui(Alignment.BottomEnd) }
-        @OptIn(ExperimentalTestApi::class)
         rule.onNodeWithTag(tag).performTouchInput { swipeLeft(endX = centerX) }
         rule.runOnIdle {
             recorder.run {
@@ -180,7 +176,6 @@
     @Test
     fun swipeRight_withParameters() {
         rule.setContent { Ui(Alignment.BottomStart) }
-        @OptIn(ExperimentalTestApi::class)
         rule.onNodeWithTag(tag).performTouchInput { swipeRight(endX = centerX) }
         rule.runOnIdle {
             recorder.run {
@@ -199,7 +194,6 @@
         expectError<IllegalArgumentException>(
             expectedMessage = "startY=0.0 needs to be greater than or equal to endY=1.0"
         ) {
-            @OptIn(ExperimentalTestApi::class)
             rule.onNodeWithTag(tag).performTouchInput { swipeUp(startY = 0f, endY = 1f) }
         }
     }
@@ -210,7 +204,6 @@
         expectError<IllegalArgumentException>(
             expectedMessage = "startY=1.0 needs to be less than or equal to endY=0.0"
         ) {
-            @OptIn(ExperimentalTestApi::class)
             rule.onNodeWithTag(tag).performTouchInput { swipeDown(startY = 1f, endY = 0f) }
         }
     }
@@ -221,7 +214,6 @@
         expectError<IllegalArgumentException>(
             expectedMessage = "startX=0.0 needs to be greater than or equal to endX=1.0"
         ) {
-            @OptIn(ExperimentalTestApi::class)
             rule.onNodeWithTag(tag).performTouchInput { swipeLeft(startX = 0f, endX = 1f) }
         }
     }
@@ -232,7 +224,6 @@
         expectError<IllegalArgumentException>(
             expectedMessage = "startX=1.0 needs to be less than or equal to endX=0.0"
         ) {
-            @OptIn(ExperimentalTestApi::class)
             rule.onNodeWithTag(tag).performTouchInput { swipeRight(startX = 1f, endX = 0f) }
         }
     }
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.android.kt
index 1099245..d6a5d94 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.android.kt
@@ -121,7 +121,6 @@
     return finalBitmap.asImageBitmap()
 }
 
-@OptIn(InternalTestApi::class)
 private fun findNodePosition(
     node: SemanticsNode
 ): Offset {
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
index 44d6f19..1f4cee0 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
@@ -37,7 +37,6 @@
 import android.view.MotionEvent.PointerProperties
 import android.view.MotionEvent.TOOL_TYPE_UNKNOWN
 import android.view.ViewConfiguration
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.nativeKeyCode
@@ -196,7 +195,6 @@
         )
     }
 
-    @OptIn(ExperimentalComposeUiApi::class)
     fun KeyInputState.constructMetaState(): Int {
 
         fun genState(key: Key, mask: Int) = if (isKeyDown(key)) mask else 0
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
index 18e4696..c03de8e 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
@@ -15,7 +15,6 @@
  */
 package androidx.compose.ui.test
 
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.node.RootForTest
@@ -824,7 +823,6 @@
      * achieved.
      */
     // TODO(Onadim): Investigate how lock key toggling is handled in Android, ChromeOS and Windows.
-    @OptIn(ExperimentalComposeUiApi::class)
     private fun updateLockKeys(key: Key) {
         when (key) {
             Key.CapsLock -> capsLockOn = !capsLockOn
diff --git a/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt
index d252f560..e74c2be 100644
--- a/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt
+++ b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt
@@ -348,8 +348,23 @@
             withAnnotation(VerbatimTtsAnnotation("verbatim2")) { append("4") }
             withAnnotation(UrlAnnotation("url1")) { append("5") }
             withAnnotation(UrlAnnotation("url2")) { append("6") }
-            withLink(LinkAnnotation.Url("url3")) { append("7") }
-            withLink(LinkAnnotation.Clickable("tag3", linkInteractionListener = null)) {
+            withLink(
+                LinkAnnotation.Url(
+                    "url3",
+                    SpanStyle(color = Color.Red),
+                    SpanStyle(color = Color.Green),
+                    SpanStyle(color = Color.Blue)
+                )
+            ) { append("7") }
+            withLink(
+                LinkAnnotation.Clickable(
+                    "tag3",
+                    SpanStyle(color = Color.Red),
+                    SpanStyle(color = Color.Green),
+                    SpanStyle(color = Color.Blue),
+                    null
+                )
+            ) {
                 append("8")
             }
         }
@@ -375,8 +390,23 @@
             withAnnotation(VerbatimTtsAnnotation("verbatim2")) { append("8") }
             withAnnotation(UrlAnnotation("url1")) { append("9") }
             withAnnotation(UrlAnnotation("url2")) { append("10") }
-            withLink(LinkAnnotation.Url("url3")) { append("11") }
-            withLink(LinkAnnotation.Clickable("tag3", linkInteractionListener = null)) {
+            withLink(
+                LinkAnnotation.Url(
+                    "url3",
+                    SpanStyle(color = Color.Red),
+                    SpanStyle(color = Color.Green),
+                    SpanStyle(color = Color.Blue)
+                )
+            ) { append("11") }
+            withLink(
+                LinkAnnotation.Clickable(
+                    "tag3",
+                    SpanStyle(color = Color.Red),
+                    SpanStyle(color = Color.Green),
+                    SpanStyle(color = Color.Blue),
+                    null
+                )
+            ) {
                 append("12")
             }
         }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
index 3e05726..4d37407 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
@@ -748,7 +748,8 @@
          * The default [Saver] implementation for [AnnotatedString].
          *
          * Note this Saver doesn't preserve the [LinkInteractionListener] of the links. You should
-         * handle this case manually if required.
+         * handle this case manually if required (check
+         * https://issuetracker.google.com/issues/332901550 for an example).
          */
         val Saver: Saver<AnnotatedString, *> = AnnotatedStringSaver
     }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
index f38e081..cdc6780 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
@@ -221,13 +221,54 @@
 )
 
 private val LinkSaver = Saver<LinkAnnotation.Url, Any>(
-    save = { save(it.url) },
-    restore = { LinkAnnotation.Url(restore(it)!!) }
+    save = {
+        arrayListOf(
+            save(it.url),
+            save(it.style, SpanStyleSaver, this),
+            save(it.focusedStyle, SpanStyleSaver, this),
+            save(it.hoveredStyle, SpanStyleSaver, this)
+        )
+    },
+    restore = {
+        val list = it as List<Any?>
+
+        val url: String = restore(list[0])!!
+        val styleOrNull: SpanStyle? = restore(list[1], SpanStyleSaver)
+        val focusedStyleOrNull: SpanStyle? = restore(list[2], SpanStyleSaver)
+        val hoveredStyleOrNull: SpanStyle? = restore(list[3], SpanStyleSaver)
+        LinkAnnotation.Url(
+            url = url,
+            style = styleOrNull,
+            focusedStyle = focusedStyleOrNull,
+            hoveredStyle = hoveredStyleOrNull
+        )
+    }
 )
 
 private val ClickableSaver = Saver<LinkAnnotation.Clickable, Any>(
-    save = { save(it.tag) },
-    restore = { LinkAnnotation.Clickable(restore(it)!!, linkInteractionListener = null) }
+    save = {
+        arrayListOf(
+            save(it.tag),
+            save(it.style, SpanStyleSaver, this),
+            save(it.focusedStyle, SpanStyleSaver, this),
+            save(it.hoveredStyle, SpanStyleSaver, this)
+        )
+    },
+    restore = {
+        val list = it as List<Any?>
+
+        val tag: String = restore(list[0])!!
+        val styleOrNull: SpanStyle? = restore(list[1], SpanStyleSaver)
+        val focusedStyleOrNull: SpanStyle? = restore(list[2], SpanStyleSaver)
+        val hoveredStyleOrNull: SpanStyle? = restore(list[3], SpanStyleSaver)
+        LinkAnnotation.Clickable(
+            tag = tag,
+            style = styleOrNull,
+            focusedStyle = focusedStyleOrNull,
+            hoveredStyle = hoveredStyleOrNull,
+            null
+        )
+    }
 )
 
 internal val ParagraphStyleSaver = Saver<ParagraphStyle, Any>(
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index e321154..12602e3 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2181,8 +2181,10 @@
     method public java.util.Set<androidx.compose.ui.layout.AlignmentLine> getProvidedAlignmentLines();
     method public long getSize();
     method public boolean isAttached();
+    method public default boolean isPositionedByParentWithDirectManipulation();
     method public androidx.compose.ui.geometry.Rect localBoundingBoxOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, optional boolean clipBounds);
     method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
+    method public default long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource, boolean excludeDirectManipulationOffset);
     method public long localToRoot(long relativeToLocal);
     method public default long localToScreen(long relativeToLocal);
     method public long localToWindow(long relativeToLocal);
@@ -2191,6 +2193,7 @@
     method public default void transformToScreen(float[] matrix);
     method public long windowToLocal(long relativeToWindow);
     property public abstract boolean isAttached;
+    property public default boolean isPositionedByParentWithDirectManipulation;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentLayoutCoordinates;
     property public abstract java.util.Set<androidx.compose.ui.layout.AlignmentLine> providedAlignmentLines;
@@ -2265,7 +2268,7 @@
 
   public interface LookaheadScope {
     method public androidx.compose.ui.layout.LayoutCoordinates getLookaheadScopeCoordinates(androidx.compose.ui.layout.Placeable.PlacementScope);
-    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default long localLookaheadPositionOf(androidx.compose.ui.layout.LayoutCoordinates, androidx.compose.ui.layout.LayoutCoordinates coordinates);
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default long localLookaheadPositionOf(androidx.compose.ui.layout.LayoutCoordinates, androidx.compose.ui.layout.LayoutCoordinates coordinates, optional boolean excludeDirectManipulationOffset);
     method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.layout.LayoutCoordinates toLookaheadCoordinates(androidx.compose.ui.layout.LayoutCoordinates);
   }
 
@@ -2415,6 +2418,7 @@
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
+    method public final void withDirectManipulationPlacement(kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> block);
     property public androidx.compose.ui.layout.LayoutCoordinates? coordinates;
     property protected abstract androidx.compose.ui.unit.LayoutDirection parentLayoutDirection;
     property protected abstract int parentWidth;
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index a3be637..b4d2e1c 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2181,8 +2181,10 @@
     method public java.util.Set<androidx.compose.ui.layout.AlignmentLine> getProvidedAlignmentLines();
     method public long getSize();
     method public boolean isAttached();
+    method public default boolean isPositionedByParentWithDirectManipulation();
     method public androidx.compose.ui.geometry.Rect localBoundingBoxOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, optional boolean clipBounds);
     method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
+    method public default long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource, boolean excludeDirectManipulationOffset);
     method public long localToRoot(long relativeToLocal);
     method public default long localToScreen(long relativeToLocal);
     method public long localToWindow(long relativeToLocal);
@@ -2191,6 +2193,7 @@
     method public default void transformToScreen(float[] matrix);
     method public long windowToLocal(long relativeToWindow);
     property public abstract boolean isAttached;
+    property public default boolean isPositionedByParentWithDirectManipulation;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentLayoutCoordinates;
     property public abstract java.util.Set<androidx.compose.ui.layout.AlignmentLine> providedAlignmentLines;
@@ -2268,7 +2271,7 @@
 
   public interface LookaheadScope {
     method public androidx.compose.ui.layout.LayoutCoordinates getLookaheadScopeCoordinates(androidx.compose.ui.layout.Placeable.PlacementScope);
-    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default long localLookaheadPositionOf(androidx.compose.ui.layout.LayoutCoordinates, androidx.compose.ui.layout.LayoutCoordinates coordinates);
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default long localLookaheadPositionOf(androidx.compose.ui.layout.LayoutCoordinates, androidx.compose.ui.layout.LayoutCoordinates coordinates, optional boolean excludeDirectManipulationOffset);
     method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.layout.LayoutCoordinates toLookaheadCoordinates(androidx.compose.ui.layout.LayoutCoordinates);
   }
 
@@ -2422,6 +2425,7 @@
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
+    method public final void withDirectManipulationPlacement(kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> block);
     property public androidx.compose.ui.layout.LayoutCoordinates? coordinates;
     property protected abstract androidx.compose.ui.unit.LayoutDirection parentLayoutDirection;
     property protected abstract int parentWidth;
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
index 21fab87..a5f1f79 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
@@ -34,7 +34,6 @@
 import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.createAndroidComposeBenchmarkRunner
 import androidx.compose.ui.platform.AndroidUiDispatcher
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -157,7 +156,7 @@
         Log.d("memoryCheckerTest", totalSum.toString())
     }
 
-    @OptIn(ExperimentalTestApi::class, ExperimentalCoroutinesApi::class)
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun recreateAndroidView_assertNoLeak() = runBlocking(AndroidUiDispatcher.Main) {
         val immediateClock = object : MonotonicFrameClock {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventSpyTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventSpyTest.kt
index 2bba86f..a22d258 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventSpyTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/MotionEventSpyTest.kt
@@ -27,7 +27,6 @@
 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.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
@@ -39,7 +38,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
+@OptIn(ExperimentalComposeUiApi::class)
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class MotionEventSpyTest {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ApproachLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ApproachLayoutTest.kt
index 0f191a2..ba4c17c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ApproachLayoutTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ApproachLayoutTest.kt
@@ -737,8 +737,8 @@
     fun testIsApproachCompleteCalledWhenSiblingRemovedInScroll() {
         var isInColumn by mutableStateOf(false)
 
-        var lastTargetPosition by mutableStateOf(Offset.Zero)
-        var lastPosition by mutableStateOf(Offset.Zero)
+        var lastTargetPosition = Offset.Zero
+        var lastPosition = Offset.Zero
 
         rule.setContent {
             CompositionLocalProvider(LocalDensity provides Density(1f)) {
@@ -852,6 +852,76 @@
         }
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun testLookaheadApproachCoordinates_togglingDirectManipulationPlacement() {
+        var toggleDmp by mutableStateOf(true)
+
+        var positionExcludingDmp = Offset.Unspecified
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LookaheadScope {
+                    Box {
+                        Box(
+                            modifier = Modifier
+                                .size(100.dp)
+                                .layout { measurable, constraints ->
+                                    // Applies a 200px offset, that may change from placing under
+                                    // DMP or not
+                                    measurable
+                                        .measure(constraints)
+                                        .run {
+                                            layout(width, height) {
+                                                if (toggleDmp) {
+                                                    withDirectManipulationPlacement {
+                                                        place(0, 200)
+                                                    }
+                                                } else {
+                                                    place(0, 200)
+                                                }
+                                            }
+                                        }
+                                }
+                                .approachLayout(
+                                    isMeasurementApproachInProgress = { false },
+                                    approachMeasure = { measurable, constraints ->
+                                        val placeable = measurable.measure(constraints)
+                                        layout(placeable.width, placeable.height) {
+                                            // Query coordinates during placement, they should
+                                            // get updated when only toggling the flag (despite
+                                            // no change in position)
+                                            coordinates?.let {
+                                                positionExcludingDmp = it
+                                                    .parentLayoutCoordinates!!
+                                                    .toLookaheadCoordinates()
+                                                    .localLookaheadPositionOf(
+                                                        coordinates = it
+                                                            .toLookaheadCoordinates(),
+                                                        excludeDirectManipulationOffset = true
+                                                    )
+                                            }
+                                            placeable.place(0, 0)
+                                        }
+                                    }
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        // Offset should be ignored
+        assertEquals(0f, positionExcludingDmp.y)
+
+        toggleDmp = !toggleDmp
+        rule.waitForIdle()
+
+        // No longer placed under DMP. No offset to ignore.
+        assertEquals(200f, positionExcludingDmp.y)
+    }
+
     private class TestPlacementScope : Placeable.PlacementScope() {
         override val parentWidth: Int
             get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 1255231..45c414a 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.animation.core.AnimationVector2D
 import androidx.compose.animation.core.VectorConverter
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement.Absolute.SpaceAround
@@ -56,6 +57,7 @@
 import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -96,6 +98,7 @@
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
 import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.util.fastRoundToInt
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import java.lang.Integer.max
@@ -109,6 +112,7 @@
 import kotlin.test.assertNotNull
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -2711,6 +2715,432 @@
         rule.waitForIdle()
     }
 
+    @Test
+    fun testDirectManipulationCoordinates_inScroll() = with(rule.density) {
+        val boxSizePx = 150f
+        val itemCount = 3
+
+        val scrollState = ScrollState(0)
+
+        // [(regularPosition, excludedPosition), ...]
+        val positionToExcludedArray = Array(3) { Offset.Unspecified to Offset.Unspecified }
+
+        rule.setContent {
+            LookaheadScope {
+                Column(
+                    Modifier
+                        // Only one box visible (can scroll up to itemCount - 1)
+                        .size(boxSizePx.toDp())
+                        .verticalScroll(scrollState)
+                ) {
+                    repeat(itemCount) { i ->
+                        Box(
+                            modifier = Modifier
+                                .size(boxSizePx.toDp())
+                                .layout { measurable, constraints ->
+                                    val placeable = measurable.measure(constraints)
+                                    layout(placeable.width, placeable.height) {
+                                        if (isLookingAhead && coordinates != null) {
+                                            val parent = coordinates!!
+                                                .parentLayoutCoordinates!!
+                                                .parentCoordinates!!
+
+                                            val position = parent
+                                                .localLookaheadPositionOf(
+                                                    coordinates = coordinates!!,
+                                                    excludeDirectManipulationOffset = false
+                                                )
+                                            val excludedPosition = parent
+                                                .localLookaheadPositionOf(
+                                                    coordinates = coordinates!!,
+                                                    excludeDirectManipulationOffset = true
+                                                )
+                                            positionToExcludedArray[i] =
+                                                position to excludedPosition
+                                        }
+                                        placeable.place(0, 0)
+                                    }
+                                }
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        // Verify initial offset, should be the same values for the "excluded" offset
+        positionToExcludedArray.forEachIndexed { index, (position, excluded) ->
+            // Rounding to avoid -0.0f
+            assertEquals((index * boxSizePx).fastRoundToInt(), position.y.fastRoundToInt())
+            assertEquals((index * boxSizePx).fastRoundToInt(), excluded.y.fastRoundToInt())
+        }
+
+        // Scroll to the end
+        runBlocking {
+            scrollState.scrollTo(((itemCount - 1) * boxSizePx).fastRoundToInt())
+        }
+        rule.waitForIdle()
+
+        // Verify positions
+        positionToExcludedArray.forEachIndexed { index, (position, excluded) ->
+            // For the default positions, we subtract the scroll amount
+            assertEquals(
+                ((index - (itemCount - 1)) * boxSizePx).fastRoundToInt(),
+                position.y.fastRoundToInt()
+            )
+
+            // The excluded should be the same as if there was no scroll
+            assertEquals((index * boxSizePx).fastRoundToInt(), excluded.y.fastRoundToInt())
+        }
+    }
+
+    @Test
+    fun testDirectManipulationCoordinates_inScroll_LookaheadChild() = with(rule.density) {
+        val boxSizePx = 150f
+        val itemCount = 3
+
+        val scrollState = ScrollState(0)
+
+        // [(regularPosition, excludedPosition), ...]
+        val positionToExcludedArray = Array(3) { Offset.Unspecified to Offset.Unspecified }
+
+        rule.setContent {
+            Column(
+                Modifier
+                    // Only one box visible (can scroll up to itemCount - 1)
+                    .size(boxSizePx.toDp())
+                    .verticalScroll(scrollState)
+            ) {
+                LookaheadScope {
+                    repeat(itemCount) { i ->
+                        Box(
+                            modifier = Modifier
+                                .size(boxSizePx.toDp())
+                                .layout { measurable, constraints ->
+                                    val placeable = measurable.measure(constraints)
+                                    layout(placeable.width, placeable.height) {
+                                        placeable.place(0, 0)
+                                        if (!isLookingAhead && coordinates != null) {
+                                            val parent = coordinates!!
+                                                .findRootCoordinates()
+
+                                            val position = parent
+                                                .localLookaheadPositionOf(
+                                                    coordinates = coordinates!!,
+                                                    excludeDirectManipulationOffset = false
+                                                )
+
+                                            val excludedPosition = parent
+                                                .localLookaheadPositionOf(
+                                                    coordinates = coordinates!!,
+                                                    excludeDirectManipulationOffset = true
+                                                )
+                                            positionToExcludedArray[i] =
+                                                position to excludedPosition
+                                        }
+                                    }
+                                }
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        // Verify initial offset, should be the same values for the "excluded" offset
+        positionToExcludedArray.forEachIndexed { index, (position, excluded) ->
+            // Rounding to avoid -0.0f
+            assertEquals((index * boxSizePx).fastRoundToInt(), position.y.fastRoundToInt())
+            assertEquals((index * boxSizePx).fastRoundToInt(), excluded.y.fastRoundToInt())
+        }
+
+        // Scroll to the end
+        runBlocking {
+            scrollState.scrollTo(((itemCount - 1) * boxSizePx).fastRoundToInt())
+        }
+        rule.waitForIdle()
+
+        // Verify positions
+        positionToExcludedArray.forEachIndexed { index, (position, excluded) ->
+            // For the default positions, we subtract the scroll amount
+            assertEquals(
+                ((index - (itemCount - 1)) * boxSizePx).fastRoundToInt(),
+                position.y.fastRoundToInt()
+            )
+
+            // The excluded should be the same as if there was no scroll
+            assertEquals((index * boxSizePx).fastRoundToInt(), excluded.y.fastRoundToInt())
+        }
+    }
+
+    @Test
+    fun testDirectManipulationCoordinates_usingModifierLayout() = with(rule.density) {
+        fun Modifier.verticalOffset(offset: Float, withDirectManipulation: Boolean) =
+            this
+                .then(
+                    object : LayoutModifier {
+                        override fun MeasureScope.measure(
+                            measurable: Measurable,
+                            constraints: Constraints
+                        ): MeasureResult {
+                            val placeable = measurable.measure(constraints)
+                            return layout(placeable.width, placeable.height) {
+                                if (withDirectManipulation) {
+                                    withDirectManipulationPlacement {
+                                        placeable.place(0, offset.fastRoundToInt())
+                                    }
+                                } else {
+                                    placeable.place(0, offset.fastRoundToInt())
+                                }
+                            }
+                        }
+                    }
+                )
+
+        var useDirectManipulation by mutableStateOf(true)
+
+        var regularPosition = Offset.Unspecified
+        var excludedManipulationPosition = Offset.Unspecified
+
+        rule.setContent {
+            LookaheadScope {
+                Box {
+                    Box(
+                        Modifier
+                            .width(100f.toDp())
+                            .height(100f.toDp())
+                            .verticalOffset(
+                                offset = 300f,
+                                withDirectManipulation = useDirectManipulation
+                            )
+                            .onPlaced {
+                                val parentLookaheadCoords = it
+                                    .parentLayoutCoordinates!!
+                                    .toLookaheadCoordinates()
+
+                                regularPosition =
+                                    parentLookaheadCoords
+                                        .localLookaheadPositionOf(
+                                            coordinates = it,
+                                            excludeDirectManipulationOffset = false
+                                        )
+
+                                excludedManipulationPosition =
+                                    parentLookaheadCoords
+                                        .localLookaheadPositionOf(
+                                            coordinates = it,
+                                            excludeDirectManipulationOffset = true
+                                        )
+                            }
+                    )
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        // When querying lookaheadPosition with `excludeDirectManipulationOffset` the offset
+        // under `withDirectManipulationPlacement` should be ignored
+        assertEquals(300f, regularPosition.y)
+        assertEquals(0f, excludedManipulationPosition.y)
+
+        // Don't place anything with direct manipulation
+        useDirectManipulation = false
+        rule.waitForIdle()
+
+        // There should be no ignored offset now.
+        assertEquals(300f, regularPosition.y)
+        assertEquals(300f, excludedManipulationPosition.y)
+    }
+
+    @Test
+    fun testDirectManipulationCoordinates_usingMeasurePolicy() {
+        class OffsetData(val offset: Float, val withDirectManipulation: Boolean)
+
+        fun Measurable.getOffsetData(): OffsetData =
+            (this.parentData as LayoutIdModifier).layoutId as OffsetData
+
+        val regularPositions = Array(2) { Offset.Unspecified }
+        val excludedManipulationPositions = Array(2) { Offset.Unspecified }
+
+        var placeWithDirectManipulation by mutableStateOf(false)
+
+        @Composable
+        fun MyLayout(modifier: Modifier, content: @Composable() (() -> Unit)) {
+            Layout(
+                modifier = modifier,
+                content = content,
+                measurePolicy = { measurables, constraints ->
+                    val placeableData = measurables.fastMap { measurable ->
+                        val data = measurable.getOffsetData()
+                        val placeable = measurable.measure(constraints)
+                        placeable to data
+                    }
+
+                    layout(300, 300) {
+                        placeableData.fastForEach { (placeable, offsetData) ->
+                            if (offsetData.withDirectManipulation) {
+                                withDirectManipulationPlacement {
+                                    placeable.place(0, offsetData.offset.fastRoundToInt())
+                                }
+                            } else {
+                                placeable.place(0, offsetData.offset.fastRoundToInt())
+                            }
+                        }
+                    }
+                }
+            )
+        }
+
+        rule.setContent {
+            LookaheadScope {
+                Column {
+                    MyLayout(
+                        modifier = Modifier.fillMaxSize()
+                    ) {
+                        Box(
+                            modifier = Modifier
+                                // starts as false
+                                .layoutId(OffsetData(100f, placeWithDirectManipulation))
+                                .onPlaced {
+                                    val parentLookaheadCoords = it
+                                        .parentLayoutCoordinates!!
+                                        .toLookaheadCoordinates()
+
+                                    regularPositions[0] =
+                                        parentLookaheadCoords
+                                            .localLookaheadPositionOf(
+                                                coordinates = it,
+                                                excludeDirectManipulationOffset = false
+                                            )
+
+                                    excludedManipulationPositions[0] =
+                                        parentLookaheadCoords
+                                            .localLookaheadPositionOf(
+                                                coordinates = it,
+                                                excludeDirectManipulationOffset = true
+                                            )
+                                }
+                        )
+                        Box(
+                            modifier = Modifier
+                                // starts as true
+                                .layoutId(OffsetData(200f, !placeWithDirectManipulation))
+                                .onPlaced {
+                                    val parentLookaheadCoords = it
+                                        .parentLayoutCoordinates!!
+                                        .toLookaheadCoordinates()
+
+                                    regularPositions[1] =
+                                        parentLookaheadCoords
+                                            .localLookaheadPositionOf(
+                                                coordinates = it,
+                                                excludeDirectManipulationOffset = false
+                                            )
+
+                                    excludedManipulationPositions[1] =
+                                        parentLookaheadCoords
+                                            .localLookaheadPositionOf(
+                                                coordinates = it,
+                                                excludeDirectManipulationOffset = true
+                                            )
+                                }
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        // For the first item, the positions are the same since it's not placed under direct
+        // manipulation
+        assertEquals(100f, regularPositions[0].y)
+        assertEquals(100f, excludedManipulationPositions[0].y)
+
+        // For the second item, we expect the offset to be ignored when excluding direct
+        // manipulation
+        assertEquals(200f, regularPositions[1].y)
+        assertEquals(0f, excludedManipulationPositions[1].y)
+
+        // Flip behaviors and re-assert
+        rule.runOnIdle {
+            placeWithDirectManipulation = !placeWithDirectManipulation
+        }
+        rule.waitForIdle()
+
+        assertEquals(100f, regularPositions[0].y)
+        assertEquals(0f, excludedManipulationPositions[0].y)
+
+        assertEquals(200f, regularPositions[1].y)
+        assertEquals(200f, excludedManipulationPositions[1].y)
+    }
+
+    @Test
+    fun testDirectManipulationCoordinates_duringPlacement() = with(rule.density) {
+        var placeWithDirectManipulation by mutableStateOf(false)
+
+        var lookingAheadPosition = Offset.Unspecified
+        var lookingAheadPositionExcludingDmp = Offset.Unspecified
+
+        rule.setContent {
+            LookaheadScope {
+                Box {
+                    Box(
+                        modifier = Modifier
+                            .size(100.toDp())
+                            // Apply offset with Direct Manipulation depending on flag
+                            .layout { measurable, constraints ->
+                                val placeable = measurable.measure(constraints)
+                                layout(placeable.width, placeable.height) {
+                                    if (placeWithDirectManipulation) {
+                                        withDirectManipulationPlacement {
+                                            placeable.place(0, 200)
+                                        }
+                                    } else {
+                                        placeable.place(0, 200)
+                                    }
+                                }
+                            }
+                            .layout { measurable, constraints ->
+                                val placeable = measurable.measure(constraints)
+
+                                layout(placeable.width, placeable.height) {
+                                    // Query lookahead coordinates during lookahead pass placement
+                                    if (isLookingAhead && coordinates != null) {
+                                        val lookaheadCoordinates =
+                                            coordinates!!.toLookaheadCoordinates()
+
+                                        lookingAheadPosition = lookaheadScopeCoordinates
+                                            .localLookaheadPositionOf(
+                                                coordinates = lookaheadCoordinates,
+                                                excludeDirectManipulationOffset = false
+                                            )
+                                        lookingAheadPositionExcludingDmp = lookaheadScopeCoordinates
+                                            .localLookaheadPositionOf(
+                                                coordinates = lookaheadCoordinates,
+                                                excludeDirectManipulationOffset = true
+                                            )
+                                    }
+                                    placeable.place(0, 0)
+                                }
+                            }
+                    )
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        // No DMP, no position to exclude
+        assertEquals(200f, lookingAheadPosition.y)
+        assertEquals(200f, lookingAheadPositionExcludingDmp.y)
+
+        placeWithDirectManipulation = true
+        rule.waitForIdle()
+
+        assertEquals(200f, lookingAheadPosition.y)
+        // Round to int, since it may return -0.0f
+        assertEquals(0, lookingAheadPositionExcludingDmp.y.fastRoundToInt())
+    }
+
     private fun assertSameLayoutWithAndWithoutLookahead(
         content: @Composable (
             modifier: Modifier
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
index a099b70..2549bf1 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
@@ -845,7 +845,6 @@
         }
     }
 
-    @OptIn(ExperimentalTestApi::class)
     @Test
     fun touchEventsAreDispatched() {
         val view = createCaptureEventsView()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
index 8f5eaa3..96fe0b4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
@@ -58,6 +58,19 @@
     val isAttached: Boolean
 
     /**
+     * Whether the coordinates were placed under direct manipulation.
+     *
+     * When true, reading [localPositionOf] coordinates with `excludeDirectManipulation = true` will
+     * exclude the offset set by its parent. This also applies when reading coordinates from a
+     * parent further up the tree, meaning, all the layouts which have this flag as `true` will not
+     * report the offset from their parent.
+     *
+     * @see Placeable.PlacementScope.withDirectManipulationPlacement
+     * @see localPositionOf
+     */
+    val isPositionedByParentWithDirectManipulation: Boolean get() = false
+
+    /**
      * Converts [relativeToScreen] relative to the device's screen's origin into an [Offset]
      * relative to this layout. Returns [Offset.Unspecified] if the conversion cannot be performed.
      */
@@ -94,6 +107,28 @@
     fun localPositionOf(sourceCoordinates: LayoutCoordinates, relativeToSource: Offset): Offset
 
     /**
+     * Converts an [relativeToSource] in [sourceCoordinates] space into local coordinates.
+     * [sourceCoordinates] may be any [LayoutCoordinates] that belong to the same
+     * compose layout hierarchy.
+     *
+     * If [excludeDirectManipulationOffset] is true, the offset provided by layouts using
+     * [Placeable.PlacementScope.withDirectManipulationPlacement] will be ignored.
+     *
+     * You can query if a [LayoutCoordinates] was placed with
+     * [Placeable.PlacementScope.withDirectManipulationPlacement] through
+     * [LayoutCoordinates.isPositionedByParentWithDirectManipulation].
+     */
+    fun localPositionOf(
+        sourceCoordinates: LayoutCoordinates,
+        relativeToSource: Offset,
+        excludeDirectManipulationOffset: Boolean
+    ): Offset {
+        throw UnsupportedOperationException(
+            "localPositionOf is not implemented on this LayoutCoordinates"
+        )
+    }
+
+    /**
      * Returns the bounding box of [sourceCoordinates] in the local coordinates.
      * If [clipBounds] is `true`, any clipping that occurs between [sourceCoordinates] and
      * this layout will affect the returned bounds, and can even result in an empty rectangle
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
index a55e3f4..569c7b6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
@@ -55,6 +55,9 @@
     override val isAttached: Boolean
         get() = coordinator.isAttached
 
+    override val isPositionedByParentWithDirectManipulation: Boolean
+        get() = lookaheadDelegate.isDirectManipulationPlacement
+
     private val lookaheadOffset: Offset
         get() = lookaheadDelegate.rootLookaheadDelegate.let {
             localPositionOf(it.coordinates, Offset.Zero) -
@@ -79,6 +82,17 @@
     override fun localPositionOf(
         sourceCoordinates: LayoutCoordinates,
         relativeToSource: Offset
+    ): Offset =
+        localPositionOf(
+            sourceCoordinates = sourceCoordinates,
+            relativeToSource = relativeToSource,
+            excludeDirectManipulationOffset = false
+        )
+
+    override fun localPositionOf(
+        sourceCoordinates: LayoutCoordinates,
+        relativeToSource: Offset,
+        excludeDirectManipulationOffset: Boolean
     ): Offset {
         if (sourceCoordinates is LookaheadLayoutCoordinates) {
             val source = sourceCoordinates.lookaheadDelegate
@@ -87,19 +101,38 @@
 
             return commonAncestor.lookaheadDelegate?.let { ancestor ->
                 // Common ancestor is in lookahead
-                (source.positionIn(ancestor) + relativeToSource.round() -
-                    lookaheadDelegate.positionIn(ancestor)).toOffset()
+                val sourceInCommonAncestor = source.positionIn(
+                    ancestor = ancestor,
+                    excludingAgnosticOffset = excludeDirectManipulationOffset
+                ) + relativeToSource.round()
+
+                val lookaheadPosInAncestor = lookaheadDelegate.positionIn(
+                    ancestor = ancestor,
+                    excludingAgnosticOffset = excludeDirectManipulationOffset
+                )
+
+                (sourceInCommonAncestor - lookaheadPosInAncestor).toOffset()
             } ?: commonAncestor.let {
                 // The two coordinates are in two separate LookaheadLayouts
                 val sourceRoot = source.rootLookaheadDelegate
-                val relativePosition = source.positionIn(sourceRoot) +
-                    sourceRoot.position + relativeToSource.round() -
-                    with(lookaheadDelegate) {
-                        (positionIn(rootLookaheadDelegate) + rootLookaheadDelegate.position)
-                    }
 
-                lookaheadDelegate.rootLookaheadDelegate.coordinator.wrappedBy!!.localPositionOf(
-                    sourceRoot.coordinator.wrappedBy!!, relativePosition.toOffset()
+                val sourcePosition = source.positionIn(
+                    ancestor = sourceRoot,
+                    excludingAgnosticOffset = excludeDirectManipulationOffset
+                ) + sourceRoot.position + relativeToSource.round()
+
+                val rootDelegate = lookaheadDelegate.rootLookaheadDelegate
+                val lookaheadPosition = lookaheadDelegate.positionIn(
+                    ancestor = rootDelegate,
+                    excludingAgnosticOffset = excludeDirectManipulationOffset
+                ) + rootDelegate.position
+
+                val relativePosition = (sourcePosition - lookaheadPosition).toOffset()
+
+                rootDelegate.coordinator.wrappedBy!!.localPositionOf(
+                    sourceCoordinates = sourceRoot.coordinator.wrappedBy!!,
+                    relativeToSource = relativePosition,
+                    excludeDirectManipulationOffset = excludeDirectManipulationOffset
                 )
             }
         } else {
@@ -108,8 +141,19 @@
             // `sourceCoordinates` isn't. Therefore we'll break this into two parts:
             // local position in lookahead coords space && local position in regular layout coords
             // space.
-            return localPositionOf(rootDelegate.lookaheadLayoutCoordinates, relativeToSource) +
-                rootDelegate.coordinator.coordinates.localPositionOf(sourceCoordinates, Offset.Zero)
+            val foo = localPositionOf(
+                sourceCoordinates = rootDelegate.lookaheadLayoutCoordinates,
+                relativeToSource = relativeToSource,
+                excludeDirectManipulationOffset = excludeDirectManipulationOffset
+            )
+
+            val bar = rootDelegate.coordinator.coordinates.localPositionOf(
+                sourceCoordinates = sourceCoordinates,
+                relativeToSource = Offset.Zero,
+                excludeDirectManipulationOffset = excludeDirectManipulationOffset
+            )
+
+            return foo + bar
         }
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadScope.kt
index bbdeb4f..cbe2216 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadScope.kt
@@ -217,11 +217,34 @@
      * converted coordinates.
      */
     @ExperimentalComposeUiApi
-    fun LayoutCoordinates.localLookaheadPositionOf(coordinates: LayoutCoordinates) =
-        this.toLookaheadCoordinates().localPositionOf(
-            coordinates.toLookaheadCoordinates(),
-            Offset.Zero
-        )
+    fun LayoutCoordinates.localLookaheadPositionOf(
+        coordinates: LayoutCoordinates,
+        excludeDirectManipulationOffset: Boolean = false,
+    ): Offset {
+        val lookaheadCoords = this.toLookaheadCoordinates()
+        val source = coordinates.toLookaheadCoordinates()
+
+        return if (lookaheadCoords is LookaheadLayoutCoordinates) {
+            lookaheadCoords.localPositionOf(
+                sourceCoordinates = source,
+                relativeToSource = Offset.Zero,
+                excludeDirectManipulationOffset = excludeDirectManipulationOffset
+            )
+        } else if (source is LookaheadLayoutCoordinates) {
+            // Relative from source, so we take its negative position
+            -source.localPositionOf(
+                sourceCoordinates = lookaheadCoords,
+                relativeToSource = Offset.Zero,
+                excludeDirectManipulationOffset = excludeDirectManipulationOffset
+            )
+        } else {
+            lookaheadCoords.localPositionOf(
+                sourceCoordinates = source,
+                relativeToSource = Offset.Zero,
+                excludeDirectManipulationOffset = excludeDirectManipulationOffset
+            )
+        }
+    }
 }
 
 @OptIn(ExperimentalComposeUiApi::class)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
index d96c185..ed87875 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.node.DirectManipulationDelegate
 import androidx.compose.ui.node.LookaheadCapablePlaceable
 import androidx.compose.ui.node.Owner
 import androidx.compose.ui.unit.Constraints
@@ -482,6 +483,7 @@
             zIndex: Float,
             noinline layerBlock: (GraphicsLayerScope.() -> Unit)?,
         ) {
+            handleDirectManipulationPlacement()
             placeAt(position + apparentToRealOffset, zIndex, layerBlock)
         }
 
@@ -491,8 +493,42 @@
             zIndex: Float,
             layer: GraphicsLayer
         ) {
+            handleDirectManipulationPlacement()
             placeAt(position + apparentToRealOffset, zIndex, layer)
         }
+
+        /**
+         * Internal indicator to know when to tag [Placeable] under direct manipulation.
+         */
+        private var directManipulationPlacement: Boolean = false
+
+        /**
+         * [Placeable]s placed under [block] may have their position excluded for lookahead
+         * coordinates, see [LookaheadLayoutCoordinates.localPositionOf] with the
+         * `excludeDirectManipulation` argument.
+         *
+         * Excluding the position set by certain layouts can be helpful to trigger lookahead based
+         * animation when intended. The typical case are layouts that change frequently due to a
+         * provided value, like [scroll][androidx.compose.foundation.verticalScroll].
+         */
+        fun withDirectManipulationPlacement(block: PlacementScope.() -> Unit) {
+            directManipulationPlacement = true
+            block()
+            directManipulationPlacement = false
+        }
+
+        /**
+         * Updates the [DirectManipulationDelegate.isDirectManipulationPlacement] flag when called
+         * a [Placeable] is placed under [withDirectManipulationPlacement].
+         *
+         * Note that the Main/Lookahead pass delegate are expected to propagate the flag to the
+         * proper [LookaheadCapablePlaceable].
+         */
+        private fun Placeable.handleDirectManipulationPlacement() {
+            if (this is DirectManipulationDelegate) {
+                this.isDirectManipulationPlacement = [email protected]
+            }
+        }
     }
 }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
index fb22252..336fd64 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
@@ -40,6 +40,7 @@
     layoutNode: LayoutNode,
     measureNode: LayoutModifierNode,
 ) : NodeCoordinator(layoutNode) {
+
     var layoutModifierNode: LayoutModifierNode = measureNode
         internal set(value) {
             if (value != field) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index e176cc7e..59645ca 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -338,8 +338,8 @@
      * [MeasurePassDelegate] manages the measure/layout and alignmentLine related queries for the
      * actual measure/layout pass.
      */
-    inner class MeasurePassDelegate : Measurable, Placeable(), AlignmentLinesOwner {
-
+    inner class MeasurePassDelegate : Measurable, Placeable(), AlignmentLinesOwner,
+        DirectManipulationDelegate {
         /**
          * Is true during [replace] invocation. Helps to differentiate between the cases when our
          * parent is measuring us during the measure block, and when we are remeasured individually
@@ -769,6 +769,26 @@
             placeSelf(position, zIndex, null, layer)
         }
 
+        /**
+         * Flag to indicate when we need to propagate coordinates updates that are not related to a
+         * position change.
+         *
+         * @see isDirectManipulationPlacement
+         */
+        private var needsCoordinatesUpdate = false
+
+        override var isDirectManipulationPlacement: Boolean = false
+            set(new) {
+                // Delegated to outerCoordinator
+                val old = outerCoordinator.isDirectManipulationPlacement
+                if (new != old) {
+                    outerCoordinator.isDirectManipulationPlacement = old
+                    // Affects coordinates measurements
+                    this.needsCoordinatesUpdate = true
+                }
+                field = new
+            }
+
         private fun placeSelf(
             position: IntOffset,
             zIndex: Float,
@@ -776,11 +796,13 @@
             layer: GraphicsLayer?
         ) {
             isPlacedByParent = true
-            if (position != lastPosition) {
+            if (position != lastPosition || needsCoordinatesUpdate) {
                 if (coordinatesAccessedDuringModifierPlacement ||
-                    coordinatesAccessedDuringPlacement
+                    coordinatesAccessedDuringPlacement ||
+                    needsCoordinatesUpdate
                 ) {
                     layoutPending = true
+                    needsCoordinatesUpdate = false
                 }
                 notifyChildrenUsingCoordinatesWhilePlacing()
             }
@@ -1101,7 +1123,8 @@
      * [LookaheadPassDelegate] manages the measure/layout and alignmentLine related queries for
      * the lookahead pass.
      */
-    inner class LookaheadPassDelegate : Placeable(), Measurable, AlignmentLinesOwner {
+    inner class LookaheadPassDelegate : Placeable(), Measurable, AlignmentLinesOwner,
+        DirectManipulationDelegate {
 
         /**
          * Is true during [replace] invocation. Helps to differentiate between the cases when our
@@ -1148,6 +1171,7 @@
             private set
 
         override var isPlaced: Boolean = false
+
         override val innerCoordinator: NodeCoordinator
             get() = layoutNode.innerCoordinator
         override val alignmentLines: AlignmentLines = LookaheadAlignmentLines(this)
@@ -1155,6 +1179,10 @@
         private val _childDelegates = MutableVector<LookaheadPassDelegate>()
 
         internal var childDelegatesDirty: Boolean = true
+
+        /**
+         * [Measurable]s provided to layout during lookahead pass.
+         */
         internal val childDelegates: List<LookaheadPassDelegate>
             get() {
                 layoutNode.children.let {
@@ -1430,6 +1458,16 @@
             placeSelf(position, zIndex, null, layer)
         }
 
+        override var isDirectManipulationPlacement: Boolean = false
+            set(new) {
+                // Delegated to outerCoordinator
+                val old = outerCoordinator.lookaheadDelegate?.isDirectManipulationPlacement
+                if (new != old) {
+                    outerCoordinator.lookaheadDelegate?.isDirectManipulationPlacement = new
+                }
+                field = new
+            }
+
         private fun placeSelf(
             position: IntOffset,
             zIndex: Float,
@@ -1894,3 +1932,22 @@
      */
     fun requestMeasure()
 }
+
+/**
+ * Interface for layout delegates, so that they can set the
+ * [LookaheadCapablePlaceable.isDirectManipulationPlacement] to the proper placeable.
+ */
+internal interface DirectManipulationDelegate {
+
+    /**
+     * Called when a layout is about to be placed.
+     *
+     * The corresponding [LookaheadCapablePlaceable] should have their
+     * [LookaheadCapablePlaceable.isDirectManipulationPlacement] flag updated to the given value.
+     *
+     * The placeable should be tagged such that its corresponding coordinates reflect the
+     * flag in [androidx.compose.ui.layout.LayoutCoordinates.isPositionedByParentWithDirectManipulation].
+     * This also means that coordinates consumers (onPlaced readers) are expected to be updated.
+     */
+    var isDirectManipulationPlacement: Boolean
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index da054fe..7038850 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -42,7 +42,8 @@
  * This is the base class for NodeCoordinator and LookaheadDelegate. The common
  * functionalities between the two are extracted here.
  */
-internal abstract class LookaheadCapablePlaceable : Placeable(), MeasureScopeWithLayoutNode {
+internal abstract class LookaheadCapablePlaceable : Placeable(), MeasureScopeWithLayoutNode,
+    DirectManipulationDelegate {
     abstract val position: IntOffset
     abstract val child: LookaheadCapablePlaceable?
     abstract val parent: LookaheadCapablePlaceable?
@@ -50,6 +51,15 @@
     abstract override val layoutNode: LayoutNode
     abstract val coordinates: LayoutCoordinates
     private var _rulerScope: RulerScope? = null
+
+    /**
+     * Indicates whether the [Placeable] was placed under direct manipulation.
+     *
+     * This means, that its offset may be ignored with [LookaheadLayoutCoordinates.localPositionOf],
+     * using the `excludeDirectManipulationOffset` parameter.
+     */
+    override var isDirectManipulationPlacement: Boolean = false
+
     val rulerScope: RulerScope
         get() {
             return _rulerScope ?: object : RulerScope {
@@ -123,6 +133,7 @@
         get() = false
 
     private var rulerValues: MutableObjectFloatMap<Ruler>? = null
+
     // For comparing before and after running the ruler lambda
     private var rulerValuesCache: MutableObjectFloatMap<Ruler>? = null
     private var rulerReaders:
@@ -461,11 +472,16 @@
         return coordinator.wrapped!!.lookaheadDelegate!!.maxIntrinsicHeight(width)
     }
 
-    internal fun positionIn(ancestor: LookaheadDelegate): IntOffset {
+    internal fun positionIn(
+        ancestor: LookaheadDelegate,
+        excludingAgnosticOffset: Boolean,
+    ): IntOffset {
         var aggregatedOffset = IntOffset.Zero
         var lookaheadDelegate = this
         while (lookaheadDelegate != ancestor) {
-            aggregatedOffset += lookaheadDelegate.position
+            if (!lookaheadDelegate.isDirectManipulationPlacement || !excludingAgnosticOffset) {
+                aggregatedOffset += lookaheadDelegate.position
+            }
             lookaheadDelegate = lookaheadDelegate.coordinator.wrappedBy!!.lookaheadDelegate!!
         }
         return aggregatedOffset
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 6f94830..64e839f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -85,6 +85,9 @@
     override val coordinates: LayoutCoordinates
         get() = this
 
+    override val isPositionedByParentWithDirectManipulation: Boolean
+        get() = isDirectManipulationPlacement
+
     private var released = false
 
     private fun headNode(includeTail: Boolean): Modifier.Node? {
@@ -830,10 +833,20 @@
     override fun localPositionOf(
         sourceCoordinates: LayoutCoordinates,
         relativeToSource: Offset
+    ): Offset = localPositionOf(sourceCoordinates, relativeToSource, false)
+
+    override fun localPositionOf(
+        sourceCoordinates: LayoutCoordinates,
+        relativeToSource: Offset,
+        excludeDirectManipulationOffset: Boolean
     ): Offset {
         if (sourceCoordinates is LookaheadLayoutCoordinates) {
             sourceCoordinates.coordinator.onCoordinatesUsed()
-            return -sourceCoordinates.localPositionOf(this, -relativeToSource)
+            return -sourceCoordinates.localPositionOf(
+                sourceCoordinates = this,
+                relativeToSource = -relativeToSource,
+                excludeDirectManipulationOffset = excludeDirectManipulationOffset
+            )
         }
 
         val nodeCoordinator = sourceCoordinates.toCoordinator()
@@ -843,11 +856,11 @@
         var position = relativeToSource
         var coordinator = nodeCoordinator
         while (coordinator !== commonAncestor) {
-            position = coordinator.toParentPosition(position)
+            position = coordinator.toParentPosition(position, excludeDirectManipulationOffset)
             coordinator = coordinator.wrappedBy!!
         }
 
-        return ancestorToLocal(commonAncestor, position)
+        return ancestorToLocal(commonAncestor, position, excludeDirectManipulationOffset)
     }
 
     override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
@@ -927,15 +940,22 @@
         return bounds.toRect()
     }
 
-    private fun ancestorToLocal(ancestor: NodeCoordinator, offset: Offset): Offset {
+    private fun ancestorToLocal(
+        ancestor: NodeCoordinator,
+        offset: Offset,
+        excludeDirectManipulationOffset: Boolean,
+    ): Offset {
         if (ancestor === this) {
             return offset
         }
         val wrappedBy = wrappedBy
         if (wrappedBy == null || ancestor == wrappedBy) {
-            return fromParentPosition(offset)
+            return fromParentPosition(offset, excludeDirectManipulationOffset)
         }
-        return fromParentPosition(wrappedBy.ancestorToLocal(ancestor, offset))
+        return fromParentPosition(
+            position = wrappedBy.ancestorToLocal(ancestor, offset, excludeDirectManipulationOffset),
+            excludeDirectManipulationOffset = excludeDirectManipulationOffset
+        )
     }
 
     private fun ancestorToLocal(
@@ -974,18 +994,33 @@
      * Converts [position] in the local coordinate system to a [Offset] in the
      * [parentLayoutCoordinates] coordinate system.
      */
-    open fun toParentPosition(position: Offset): Offset {
+    open fun toParentPosition(
+        position: Offset,
+        excludeDirectManipulationOffset: Boolean = false
+    ): Offset {
         val layer = layer
         val targetPosition = layer?.mapOffset(position, inverse = false) ?: position
-        return targetPosition + this.position
+        return if (excludeDirectManipulationOffset && isDirectManipulationPlacement) {
+            targetPosition
+        } else {
+            targetPosition + this.position
+        }
     }
 
     /**
      * Converts [position] in the [parentLayoutCoordinates] coordinate system to a [Offset] in the
      * local coordinate system.
      */
-    open fun fromParentPosition(position: Offset): Offset {
-        val relativeToPosition = position - this.position
+    open fun fromParentPosition(
+        position: Offset,
+        excludeDirectManipulationOffset: Boolean = false
+    ): Offset {
+        val relativeToPosition =
+            if (excludeDirectManipulationOffset && isDirectManipulationPlacement) {
+                position
+            } else {
+                position - this.position
+            }
         val layer = layer
         return layer?.mapOffset(relativeToPosition, inverse = true)
             ?: relativeToPosition
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
index be0c4a9..c47fe8d 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
@@ -34,7 +34,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
@@ -42,7 +41,6 @@
 import org.junit.Rule
 import org.junit.Test
 
-@OptIn(ExperimentalTestApi::class)
 class DesktopPopupTest {
     @get:Rule
     val rule = createComposeRule()
diff --git a/core/core-location-altitude/proguard-rules.pro b/core/core-location-altitude/proguard-rules.pro
index dee5477..c610137 100644
--- a/core/core-location-altitude/proguard-rules.pro
+++ b/core/core-location-altitude/proguard-rules.pro
@@ -15,5 +15,6 @@
 # libproto uses reflection to deserialize a Proto, which Proguard can't accurately detect.
 # Keep all the class members of any generated messages to ensure we can deserialize properly inside
 # these classes.
--if class * extends androidx.core.location.altitude.impl.proto.GeneratedMessageLite
--keepclasseswithmembers
\ No newline at end of file
+-keepclassmembers class * extends androidx.core.location.altitude.impl.proto.GeneratedMessageLite {
+  <fields>;
+}
diff --git a/core/core-remoteviews/api/1.1.0-beta02.txt b/core/core-remoteviews/api/1.1.0-beta02.txt
new file mode 100644
index 0000000..8bd3b49
--- /dev/null
+++ b/core/core-remoteviews/api/1.1.0-beta02.txt
@@ -0,0 +1,294 @@
+// Signature format: 4.0
+package androidx.core.widget {
+
+  public final class AppWidgetManagerCompat {
+    method public static android.widget.RemoteViews createExactSizeAppWidget(android.appwidget.AppWidgetManager appWidgetManager, int appWidgetId, kotlin.jvm.functions.Function1<? super androidx.core.util.SizeFCompat,? extends android.widget.RemoteViews> factory);
+    method public static android.widget.RemoteViews createResponsiveSizeAppWidget(android.appwidget.AppWidgetManager appWidgetManager, int appWidgetId, java.util.Collection<androidx.core.util.SizeFCompat> dpSizes, kotlin.jvm.functions.Function1<? super androidx.core.util.SizeFCompat,? extends android.widget.RemoteViews> factory);
+    method public static void updateAppWidget(android.appwidget.AppWidgetManager, int appWidgetId, java.util.Collection<androidx.core.util.SizeFCompat> dpSizes, kotlin.jvm.functions.Function1<? super androidx.core.util.SizeFCompat,? extends android.widget.RemoteViews> factory);
+    method public static void updateAppWidget(android.appwidget.AppWidgetManager, int appWidgetId, kotlin.jvm.functions.Function1<? super androidx.core.util.SizeFCompat,? extends android.widget.RemoteViews> factory);
+  }
+
+  public final class RemoteViewsCompat {
+    method public static void setChronometerBase(android.widget.RemoteViews, @IdRes int viewId, long base);
+    method public static void setChronometerFormat(android.widget.RemoteViews, @IdRes int viewId, String? format);
+    method @RequiresApi(31) public static void setCompoundButtonDrawable(android.widget.RemoteViews, @IdRes int viewId, @DrawableRes int resId);
+    method @RequiresApi(31) public static void setCompoundButtonIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? icon);
+    method @RequiresApi(31) public static void setCompoundButtonTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? tintMode);
+    method @RequiresApi(31) public static void setCompoundButtonTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setCompoundButtonTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNight, android.content.res.ColorStateList? night);
+    method @RequiresApi(31) public static void setCompoundButtonTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setCompoundButtonTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setFrameLayoutForegroundGravity(android.widget.RemoteViews, @IdRes int viewId, int foregroundGravity);
+    method public static void setFrameLayoutMeasureAllChildren(android.widget.RemoteViews, @IdRes int viewId, boolean measureAll);
+    method @RequiresApi(31) public static void setGridLayoutAlignmentMode(android.widget.RemoteViews, @IdRes int viewId, int alignmentMode);
+    method @RequiresApi(31) public static void setGridLayoutColumnCount(android.widget.RemoteViews, @IdRes int viewId, int columnCount);
+    method @RequiresApi(31) public static void setGridLayoutRowCount(android.widget.RemoteViews, @IdRes int viewId, int rowCount);
+    method @RequiresApi(31) public static void setGridViewColumnWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setGridViewColumnWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int columnWidth);
+    method @RequiresApi(31) public static void setGridViewColumnWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int columnWidth);
+    method @RequiresApi(31) public static void setGridViewGravity(android.widget.RemoteViews, @IdRes int viewId, int gravity);
+    method @RequiresApi(31) public static void setGridViewHorizontalSpacing(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setGridViewHorizontalSpacingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setGridViewHorizontalSpacingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setGridViewNumColumns(android.widget.RemoteViews, @IdRes int viewId, int numColumns);
+    method @RequiresApi(31) public static void setGridViewStretchMode(android.widget.RemoteViews, @IdRes int viewId, int stretchMode);
+    method @RequiresApi(31) public static void setGridViewVerticalSpacing(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setGridViewVerticalSpacingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setGridViewVerticalSpacingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setImageViewAdjustViewBounds(android.widget.RemoteViews, @IdRes int viewId, boolean adjustViewBounds);
+    method public static void setImageViewColorFilter(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setImageViewColorFilter(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setImageViewColorFilterAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setImageViewColorFilterResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setImageViewImageAlpha(android.widget.RemoteViews, @IdRes int viewId, int alpha);
+    method public static void setImageViewImageLevel(android.widget.RemoteViews, @IdRes int viewId, int level);
+    method @RequiresApi(31) public static void setImageViewImageTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setImageViewImageTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setImageViewImageTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setImageViewImageTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setImageViewImageTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setImageViewMaxHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setImageViewMaxHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int maxHeight);
+    method @RequiresApi(31) public static void setImageViewMaxHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setImageViewMaxHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setImageViewMaxWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setImageViewMaxWidth(android.widget.RemoteViews, @IdRes int viewId, @Px int maxWidth);
+    method @RequiresApi(31) public static void setImageViewMaxWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setImageViewMaxWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setLinearLayoutBaselineAligned(android.widget.RemoteViews, @IdRes int viewId, boolean baselineAligned);
+    method public static void setLinearLayoutBaselineAlignedChildIndex(android.widget.RemoteViews, @IdRes int viewId, int i);
+    method public static void setLinearLayoutGravity(android.widget.RemoteViews, @IdRes int viewId, int gravity);
+    method public static void setLinearLayoutHorizontalGravity(android.widget.RemoteViews, @IdRes int viewId, int horizontalGravity);
+    method public static void setLinearLayoutMeasureWithLargestChildEnabled(android.widget.RemoteViews, @IdRes int viewId, boolean enabled);
+    method public static void setLinearLayoutVerticalGravity(android.widget.RemoteViews, @IdRes int viewId, int verticalGravity);
+    method public static void setLinearLayoutWeightSum(android.widget.RemoteViews, @IdRes int viewId, float weightSum);
+    method public static void setProgressBarIndeterminate(android.widget.RemoteViews, @IdRes int viewId, boolean indeterminate);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setProgressBarMax(android.widget.RemoteViews, @IdRes int viewId, int max);
+    method @RequiresApi(26) public static void setProgressBarMin(android.widget.RemoteViews, @IdRes int viewId, int min);
+    method public static void setProgressBarProgress(android.widget.RemoteViews, @IdRes int viewId, int progress);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setProgressBarProgressTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setProgressBarProgressTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setProgressBarProgressTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setProgressBarProgressTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setProgressBarProgressTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setProgressBarSecondaryProgress(android.widget.RemoteViews, @IdRes int viewId, int secondaryProgress);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setProgressBarStateDescription(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(31) public static void setProgressBarStateDescription(android.widget.RemoteViews, @IdRes int viewId, CharSequence? stateDescription);
+    method @RequiresApi(31) public static void setProgressBarStateDescriptionAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setRelativeLayoutGravity(android.widget.RemoteViews, @IdRes int viewId, int gravity);
+    method public static void setRelativeLayoutHorizontalGravity(android.widget.RemoteViews, @IdRes int viewId, int horizontalGravity);
+    method public static void setRelativeLayoutIgnoreGravity(android.widget.RemoteViews, @IdRes int viewId, @IdRes int childViewId);
+    method public static void setRelativeLayoutVerticalGravity(android.widget.RemoteViews, @IdRes int viewId, int verticalGravity);
+    method public static void setRemoteAdapter(android.content.Context context, android.widget.RemoteViews remoteViews, int appWidgetId, @IdRes int viewId, androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems items);
+    method @RequiresApi(31) public static void setSwitchMinWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setSwitchMinWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setSwitchMinWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchPadding(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setSwitchPaddingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setSwitchPaddingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchShowText(android.widget.RemoteViews, @IdRes int viewId, boolean showText);
+    method @RequiresApi(31) public static void setSwitchSplitTrack(android.widget.RemoteViews, @IdRes int viewId, boolean splitTrack);
+    method @RequiresApi(31) public static void setSwitchTextOff(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(31) public static void setSwitchTextOff(android.widget.RemoteViews, @IdRes int viewId, CharSequence? textOff);
+    method @RequiresApi(31) public static void setSwitchTextOffAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchTextOn(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(31) public static void setSwitchTextOn(android.widget.RemoteViews, @IdRes int viewId, CharSequence? textOn);
+    method @RequiresApi(31) public static void setSwitchTextOnAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? icon);
+    method @RequiresApi(31) public static void setSwitchThumbIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? notNight, android.graphics.drawable.Icon? night);
+    method @RequiresApi(31) public static void setSwitchThumbResource(android.widget.RemoteViews, @IdRes int viewId, @DrawableRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbTextPadding(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setSwitchThumbTextPaddingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbTextPaddingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setSwitchThumbTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setSwitchThumbTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNight, android.content.res.ColorStateList? night);
+    method @RequiresApi(31) public static void setSwitchThumbTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchTrackIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? icon);
+    method @RequiresApi(31) public static void setSwitchTrackIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? notNight, android.graphics.drawable.Icon? night);
+    method @RequiresApi(31) public static void setSwitchTrackResource(android.widget.RemoteViews, @IdRes int viewId, @DrawableRes int resId);
+    method @RequiresApi(31) public static void setSwitchTrackTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setSwitchTrackTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setSwitchTrackTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNight, android.content.res.ColorStateList? night);
+    method @RequiresApi(31) public static void setSwitchTrackTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setSwitchTrackTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextClockFormat12Hour(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setTextClockFormat12Hour(android.widget.RemoteViews, @IdRes int viewId, CharSequence? format);
+    method @RequiresApi(31) public static void setTextClockFormat12HourAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextClockFormat24Hour(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setTextClockFormat24Hour(android.widget.RemoteViews, @IdRes int viewId, CharSequence? format);
+    method @RequiresApi(31) public static void setTextClockFormat24HourAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextClockTimeZone(android.widget.RemoteViews, @IdRes int viewId, String? timeZone);
+    method @RequiresApi(31) public static void setTextViewAllCaps(android.widget.RemoteViews, @IdRes int viewId, boolean allCaps);
+    method public static void setTextViewAutoLinkMask(android.widget.RemoteViews, @IdRes int viewId, int mask);
+    method @RequiresApi(31) public static void setTextViewCompoundDrawablePadding(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewCompoundDrawablePadding(android.widget.RemoteViews, @IdRes int viewId, @Px int pad);
+    method @RequiresApi(31) public static void setTextViewCompoundDrawablePaddingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewCompoundDrawablePaddingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewEms(android.widget.RemoteViews, @IdRes int viewId, int ems);
+    method @RequiresApi(31) public static void setTextViewError(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setTextViewError(android.widget.RemoteViews, @IdRes int viewId, CharSequence? error);
+    method @RequiresApi(31) public static void setTextViewErrorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(21) public static void setTextViewFontFeatureSettings(android.widget.RemoteViews, @IdRes int viewId, String fontFeatureSettings);
+    method @RequiresApi(31) public static void setTextViewGravity(android.widget.RemoteViews, @IdRes int viewId, int gravity);
+    method @RequiresApi(31) public static void setTextViewHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int pixels);
+    method @RequiresApi(31) public static void setTextViewHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewHighlightColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setTextViewHighlightColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setTextViewHighlightColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewHighlightColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setTextViewHint(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setTextViewHint(android.widget.RemoteViews, @IdRes int viewId, CharSequence? hint);
+    method @RequiresApi(31) public static void setTextViewHintAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewHintTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setTextViewHintTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setTextViewHintTextColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewHintTextColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setTextViewJustificationMode(android.widget.RemoteViews, @IdRes int viewId, int justificationMode);
+    method @RequiresApi(21) public static void setTextViewLetterSpacing(android.widget.RemoteViews, @IdRes int viewId, float letterSpacing);
+    method @RequiresApi(31) public static void setTextViewLineHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setTextViewLineHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewLineHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewLines(android.widget.RemoteViews, @IdRes int viewId, int lines);
+    method public static void setTextViewLinkTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setTextViewLinkTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setTextViewLinkTextColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewLinkTextColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setTextViewLinksClickable(android.widget.RemoteViews, @IdRes int viewId, boolean whether);
+    method public static void setTextViewMaxEms(android.widget.RemoteViews, @IdRes int viewId, int maxems);
+    method @RequiresApi(31) public static void setTextViewMaxHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewMaxHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int maxHeight);
+    method @RequiresApi(31) public static void setTextViewMaxHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewMaxHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewMaxLines(android.widget.RemoteViews, @IdRes int viewId, int maxLines);
+    method @RequiresApi(31) public static void setTextViewMaxWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewMaxWidth(android.widget.RemoteViews, @IdRes int viewId, @Px int maxWidth);
+    method @RequiresApi(31) public static void setTextViewMaxWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewMaxWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewMinEms(android.widget.RemoteViews, @IdRes int viewId, int minems);
+    method @RequiresApi(31) public static void setTextViewMinHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewMinHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int minHeight);
+    method @RequiresApi(31) public static void setTextViewMinHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewMinHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewMinLines(android.widget.RemoteViews, @IdRes int viewId, int minLines);
+    method @RequiresApi(31) public static void setTextViewMinWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewMinWidth(android.widget.RemoteViews, @IdRes int viewId, @Px int minWidth);
+    method @RequiresApi(31) public static void setTextViewMinWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewMinWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewPaintFlags(android.widget.RemoteViews, @IdRes int viewId, int flags);
+    method public static void setTextViewSelectAllOnFocus(android.widget.RemoteViews, @IdRes int viewId, boolean selectAllOnFocus);
+    method public static void setTextViewSingleLine(android.widget.RemoteViews, @IdRes int viewId, boolean singleLine);
+    method public static void setTextViewText(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(31) public static void setTextViewTextAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewTextColor(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList colors);
+    method @RequiresApi(31) public static void setTextViewTextColor(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList notNight, android.content.res.ColorStateList night);
+    method public static void setTextViewTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setTextViewTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setTextViewTextColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewTextColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setTextViewTextScaleX(android.widget.RemoteViews, @IdRes int viewId, float size);
+    method @RequiresApi(31) public static void setTextViewTextSizeDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewTextSizeDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewWidth(android.widget.RemoteViews, @IdRes int viewId, @Px int pixels);
+    method @RequiresApi(31) public static void setTextViewWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewAlpha(android.widget.RemoteViews, @IdRes int viewId, float alpha);
+    method public static void setViewBackgroundColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setViewBackgroundColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setViewBackgroundColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setViewBackgroundColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setViewBackgroundResource(android.widget.RemoteViews, @IdRes int viewId, @DrawableRes int resId);
+    method @RequiresApi(31) public static void setViewBackgroundTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setViewBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setViewBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setViewBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setViewBackgroundTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewClipToOutline(android.widget.RemoteViews, @IdRes int viewId, boolean clipToOutline);
+    method @RequiresApi(31) public static void setViewContentDescription(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setViewContentDescription(android.widget.RemoteViews, @IdRes int viewId, CharSequence? contentDescription);
+    method @RequiresApi(31) public static void setViewContentDescriptionAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewElevationDimen(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewElevationDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewElevationDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(24) public static void setViewEnabled(android.widget.RemoteViews, @IdRes int viewId, boolean enabled);
+    method @RequiresApi(31) public static void setViewFocusable(android.widget.RemoteViews, @IdRes int viewId, boolean focusable);
+    method @RequiresApi(31) public static void setViewFocusable(android.widget.RemoteViews, @IdRes int viewId, int focusable);
+    method @RequiresApi(31) public static void setViewFocusableInTouchMode(android.widget.RemoteViews, @IdRes int viewId, boolean focusableInTouchMode);
+    method @RequiresApi(31) public static void setViewFocusedByDefault(android.widget.RemoteViews, @IdRes int viewId, boolean isFocusedByDefault);
+    method @RequiresApi(31) public static void setViewForegroundTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setViewForegroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setViewForegroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setViewForegroundTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setViewForegroundTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setViewLayoutDirection(android.widget.RemoteViews, @IdRes int viewId, int layoutDirection);
+    method @RequiresApi(31) public static void setViewMinimumHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(24) public static void setViewMinimumHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int minHeight);
+    method @RequiresApi(31) public static void setViewMinimumHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewMinimumHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewMinimumWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewMinimumWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewMinimumWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewPivotX(android.widget.RemoteViews, @IdRes int viewId, float pivotX);
+    method @RequiresApi(31) public static void setViewPivotY(android.widget.RemoteViews, @IdRes int viewId, float pivotY);
+    method @RequiresApi(31) public static void setViewRotation(android.widget.RemoteViews, @IdRes int viewId, float rotation);
+    method @RequiresApi(31) public static void setViewRotationX(android.widget.RemoteViews, @IdRes int viewId, float rotationX);
+    method @RequiresApi(31) public static void setViewRotationY(android.widget.RemoteViews, @IdRes int viewId, float rotationY);
+    method @RequiresApi(31) public static void setViewScaleX(android.widget.RemoteViews, @IdRes int viewId, float scaleX);
+    method @RequiresApi(31) public static void setViewScaleY(android.widget.RemoteViews, @IdRes int viewId, float scaleY);
+    method @RequiresApi(31) public static void setViewScrollIndicators(android.widget.RemoteViews, @IdRes int viewId, int scrollIndicators);
+    method @RequiresApi(31) public static void setViewStateDescription(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(30) public static void setViewStateDescription(android.widget.RemoteViews, @IdRes int viewId, CharSequence? stateDescription);
+    method @RequiresApi(31) public static void setViewStateDescriptionAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setViewStubInflatedId(android.widget.RemoteViews, @IdRes int viewId, int inflatedId);
+    method public static void setViewStubLayoutResource(android.widget.RemoteViews, @IdRes int viewId, @LayoutRes int layoutResource);
+    method @RequiresApi(31) public static void setViewTranslationXDimen(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewTranslationXDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationXDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationYDimen(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewTranslationYDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationYDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationZDimen(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewTranslationZDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationZDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    field public static final androidx.core.widget.RemoteViewsCompat INSTANCE;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems {
+    method public int getItemCount();
+    method public long getItemId(int position);
+    method public android.widget.RemoteViews getItemView(int position);
+    method public int getViewTypeCount();
+    method public boolean hasStableIds();
+    property public final int itemCount;
+    property public final int viewTypeCount;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems.Builder {
+    ctor public RemoteViewsCompat.RemoteCollectionItems.Builder();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder addItem(long id, android.widget.RemoteViews view);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems build();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setHasStableIds(boolean hasStableIds);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setViewTypeCount(int viewTypeCount);
+  }
+
+}
+
diff --git a/core/core-remoteviews/api/res-1.1.0-beta02.txt b/core/core-remoteviews/api/res-1.1.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/core-remoteviews/api/res-1.1.0-beta02.txt
diff --git a/core/core-remoteviews/api/restricted_1.1.0-beta02.txt b/core/core-remoteviews/api/restricted_1.1.0-beta02.txt
new file mode 100644
index 0000000..07071af
--- /dev/null
+++ b/core/core-remoteviews/api/restricted_1.1.0-beta02.txt
@@ -0,0 +1,299 @@
+// Signature format: 4.0
+package androidx.core.widget {
+
+  public final class AppWidgetManagerCompat {
+    method public static android.widget.RemoteViews createExactSizeAppWidget(android.appwidget.AppWidgetManager appWidgetManager, int appWidgetId, kotlin.jvm.functions.Function1<? super androidx.core.util.SizeFCompat,? extends android.widget.RemoteViews> factory);
+    method public static android.widget.RemoteViews createResponsiveSizeAppWidget(android.appwidget.AppWidgetManager appWidgetManager, int appWidgetId, java.util.Collection<androidx.core.util.SizeFCompat> dpSizes, kotlin.jvm.functions.Function1<? super androidx.core.util.SizeFCompat,? extends android.widget.RemoteViews> factory);
+    method public static void updateAppWidget(android.appwidget.AppWidgetManager, int appWidgetId, java.util.Collection<androidx.core.util.SizeFCompat> dpSizes, kotlin.jvm.functions.Function1<? super androidx.core.util.SizeFCompat,? extends android.widget.RemoteViews> factory);
+    method public static void updateAppWidget(android.appwidget.AppWidgetManager, int appWidgetId, kotlin.jvm.functions.Function1<? super androidx.core.util.SizeFCompat,? extends android.widget.RemoteViews> factory);
+  }
+
+  public final class RemoteViewsCompat {
+    method public static void setChronometerBase(android.widget.RemoteViews, @IdRes int viewId, long base);
+    method public static void setChronometerFormat(android.widget.RemoteViews, @IdRes int viewId, String? format);
+    method @RequiresApi(31) public static void setCompoundButtonDrawable(android.widget.RemoteViews, @IdRes int viewId, @DrawableRes int resId);
+    method @RequiresApi(31) public static void setCompoundButtonIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? icon);
+    method @RequiresApi(31) public static void setCompoundButtonTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? tintMode);
+    method @RequiresApi(31) public static void setCompoundButtonTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setCompoundButtonTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNight, android.content.res.ColorStateList? night);
+    method @RequiresApi(31) public static void setCompoundButtonTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setCompoundButtonTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setFrameLayoutForegroundGravity(android.widget.RemoteViews, @IdRes int viewId, int foregroundGravity);
+    method public static void setFrameLayoutMeasureAllChildren(android.widget.RemoteViews, @IdRes int viewId, boolean measureAll);
+    method @RequiresApi(31) public static void setGridLayoutAlignmentMode(android.widget.RemoteViews, @IdRes int viewId, int alignmentMode);
+    method @RequiresApi(31) public static void setGridLayoutColumnCount(android.widget.RemoteViews, @IdRes int viewId, int columnCount);
+    method @RequiresApi(31) public static void setGridLayoutRowCount(android.widget.RemoteViews, @IdRes int viewId, int rowCount);
+    method @RequiresApi(31) public static void setGridViewColumnWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setGridViewColumnWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int columnWidth);
+    method @RequiresApi(31) public static void setGridViewColumnWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int columnWidth);
+    method @RequiresApi(31) public static void setGridViewGravity(android.widget.RemoteViews, @IdRes int viewId, int gravity);
+    method @RequiresApi(31) public static void setGridViewHorizontalSpacing(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setGridViewHorizontalSpacingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setGridViewHorizontalSpacingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setGridViewNumColumns(android.widget.RemoteViews, @IdRes int viewId, int numColumns);
+    method @RequiresApi(31) public static void setGridViewStretchMode(android.widget.RemoteViews, @IdRes int viewId, int stretchMode);
+    method @RequiresApi(31) public static void setGridViewVerticalSpacing(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setGridViewVerticalSpacingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setGridViewVerticalSpacingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setImageViewAdjustViewBounds(android.widget.RemoteViews, @IdRes int viewId, boolean adjustViewBounds);
+    method public static void setImageViewColorFilter(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setImageViewColorFilter(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setImageViewColorFilterAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setImageViewColorFilterResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setImageViewImageAlpha(android.widget.RemoteViews, @IdRes int viewId, int alpha);
+    method public static void setImageViewImageLevel(android.widget.RemoteViews, @IdRes int viewId, int level);
+    method @RequiresApi(31) public static void setImageViewImageTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setImageViewImageTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setImageViewImageTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setImageViewImageTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setImageViewImageTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setImageViewMaxHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setImageViewMaxHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int maxHeight);
+    method @RequiresApi(31) public static void setImageViewMaxHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setImageViewMaxHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setImageViewMaxWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setImageViewMaxWidth(android.widget.RemoteViews, @IdRes int viewId, @Px int maxWidth);
+    method @RequiresApi(31) public static void setImageViewMaxWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setImageViewMaxWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setLinearLayoutBaselineAligned(android.widget.RemoteViews, @IdRes int viewId, boolean baselineAligned);
+    method public static void setLinearLayoutBaselineAlignedChildIndex(android.widget.RemoteViews, @IdRes int viewId, int i);
+    method public static void setLinearLayoutGravity(android.widget.RemoteViews, @IdRes int viewId, int gravity);
+    method public static void setLinearLayoutHorizontalGravity(android.widget.RemoteViews, @IdRes int viewId, int horizontalGravity);
+    method public static void setLinearLayoutMeasureWithLargestChildEnabled(android.widget.RemoteViews, @IdRes int viewId, boolean enabled);
+    method public static void setLinearLayoutVerticalGravity(android.widget.RemoteViews, @IdRes int viewId, int verticalGravity);
+    method public static void setLinearLayoutWeightSum(android.widget.RemoteViews, @IdRes int viewId, float weightSum);
+    method public static void setProgressBarIndeterminate(android.widget.RemoteViews, @IdRes int viewId, boolean indeterminate);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setProgressBarIndeterminateTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setProgressBarMax(android.widget.RemoteViews, @IdRes int viewId, int max);
+    method @RequiresApi(26) public static void setProgressBarMin(android.widget.RemoteViews, @IdRes int viewId, int min);
+    method public static void setProgressBarProgress(android.widget.RemoteViews, @IdRes int viewId, int progress);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setProgressBarProgressBackgroundTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setProgressBarProgressTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setProgressBarProgressTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setProgressBarProgressTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setProgressBarProgressTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setProgressBarProgressTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setProgressBarSecondaryProgress(android.widget.RemoteViews, @IdRes int viewId, int secondaryProgress);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setProgressBarSecondaryProgressTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setProgressBarStateDescription(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(31) public static void setProgressBarStateDescription(android.widget.RemoteViews, @IdRes int viewId, CharSequence? stateDescription);
+    method @RequiresApi(31) public static void setProgressBarStateDescriptionAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setRelativeLayoutGravity(android.widget.RemoteViews, @IdRes int viewId, int gravity);
+    method public static void setRelativeLayoutHorizontalGravity(android.widget.RemoteViews, @IdRes int viewId, int horizontalGravity);
+    method public static void setRelativeLayoutIgnoreGravity(android.widget.RemoteViews, @IdRes int viewId, @IdRes int childViewId);
+    method public static void setRelativeLayoutVerticalGravity(android.widget.RemoteViews, @IdRes int viewId, int verticalGravity);
+    method public static void setRemoteAdapter(android.content.Context context, android.widget.RemoteViews remoteViews, int appWidgetId, @IdRes int viewId, androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems items);
+    method @RequiresApi(31) public static void setSwitchMinWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setSwitchMinWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setSwitchMinWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchPadding(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setSwitchPaddingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setSwitchPaddingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchShowText(android.widget.RemoteViews, @IdRes int viewId, boolean showText);
+    method @RequiresApi(31) public static void setSwitchSplitTrack(android.widget.RemoteViews, @IdRes int viewId, boolean splitTrack);
+    method @RequiresApi(31) public static void setSwitchTextOff(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(31) public static void setSwitchTextOff(android.widget.RemoteViews, @IdRes int viewId, CharSequence? textOff);
+    method @RequiresApi(31) public static void setSwitchTextOffAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchTextOn(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(31) public static void setSwitchTextOn(android.widget.RemoteViews, @IdRes int viewId, CharSequence? textOn);
+    method @RequiresApi(31) public static void setSwitchTextOnAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? icon);
+    method @RequiresApi(31) public static void setSwitchThumbIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? notNight, android.graphics.drawable.Icon? night);
+    method @RequiresApi(31) public static void setSwitchThumbResource(android.widget.RemoteViews, @IdRes int viewId, @DrawableRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbTextPadding(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setSwitchThumbTextPaddingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbTextPaddingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setSwitchThumbTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setSwitchThumbTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNight, android.content.res.ColorStateList? night);
+    method @RequiresApi(31) public static void setSwitchThumbTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setSwitchThumbTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setSwitchTrackIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? icon);
+    method @RequiresApi(31) public static void setSwitchTrackIcon(android.widget.RemoteViews, @IdRes int viewId, android.graphics.drawable.Icon? notNight, android.graphics.drawable.Icon? night);
+    method @RequiresApi(31) public static void setSwitchTrackResource(android.widget.RemoteViews, @IdRes int viewId, @DrawableRes int resId);
+    method @RequiresApi(31) public static void setSwitchTrackTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setSwitchTrackTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setSwitchTrackTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNight, android.content.res.ColorStateList? night);
+    method @RequiresApi(31) public static void setSwitchTrackTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setSwitchTrackTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextClockFormat12Hour(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setTextClockFormat12Hour(android.widget.RemoteViews, @IdRes int viewId, CharSequence? format);
+    method @RequiresApi(31) public static void setTextClockFormat12HourAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextClockFormat24Hour(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setTextClockFormat24Hour(android.widget.RemoteViews, @IdRes int viewId, CharSequence? format);
+    method @RequiresApi(31) public static void setTextClockFormat24HourAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextClockTimeZone(android.widget.RemoteViews, @IdRes int viewId, String? timeZone);
+    method @RequiresApi(31) public static void setTextViewAllCaps(android.widget.RemoteViews, @IdRes int viewId, boolean allCaps);
+    method public static void setTextViewAutoLinkMask(android.widget.RemoteViews, @IdRes int viewId, int mask);
+    method @RequiresApi(31) public static void setTextViewCompoundDrawablePadding(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewCompoundDrawablePadding(android.widget.RemoteViews, @IdRes int viewId, @Px int pad);
+    method @RequiresApi(31) public static void setTextViewCompoundDrawablePaddingDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewCompoundDrawablePaddingDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewEms(android.widget.RemoteViews, @IdRes int viewId, int ems);
+    method @RequiresApi(31) public static void setTextViewError(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setTextViewError(android.widget.RemoteViews, @IdRes int viewId, CharSequence? error);
+    method @RequiresApi(31) public static void setTextViewErrorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(21) public static void setTextViewFontFeatureSettings(android.widget.RemoteViews, @IdRes int viewId, String fontFeatureSettings);
+    method @RequiresApi(31) public static void setTextViewGravity(android.widget.RemoteViews, @IdRes int viewId, int gravity);
+    method @RequiresApi(31) public static void setTextViewHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int pixels);
+    method @RequiresApi(31) public static void setTextViewHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewHighlightColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setTextViewHighlightColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setTextViewHighlightColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewHighlightColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setTextViewHint(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setTextViewHint(android.widget.RemoteViews, @IdRes int viewId, CharSequence? hint);
+    method @RequiresApi(31) public static void setTextViewHintAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewHintTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setTextViewHintTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setTextViewHintTextColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewHintTextColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setTextViewJustificationMode(android.widget.RemoteViews, @IdRes int viewId, int justificationMode);
+    method @RequiresApi(21) public static void setTextViewLetterSpacing(android.widget.RemoteViews, @IdRes int viewId, float letterSpacing);
+    method @RequiresApi(31) public static void setTextViewLineHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setTextViewLineHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewLineHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewLines(android.widget.RemoteViews, @IdRes int viewId, int lines);
+    method public static void setTextViewLinkTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setTextViewLinkTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setTextViewLinkTextColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewLinkTextColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setTextViewLinksClickable(android.widget.RemoteViews, @IdRes int viewId, boolean whether);
+    method public static void setTextViewMaxEms(android.widget.RemoteViews, @IdRes int viewId, int maxems);
+    method @RequiresApi(31) public static void setTextViewMaxHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewMaxHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int maxHeight);
+    method @RequiresApi(31) public static void setTextViewMaxHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewMaxHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewMaxLines(android.widget.RemoteViews, @IdRes int viewId, int maxLines);
+    method @RequiresApi(31) public static void setTextViewMaxWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewMaxWidth(android.widget.RemoteViews, @IdRes int viewId, @Px int maxWidth);
+    method @RequiresApi(31) public static void setTextViewMaxWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewMaxWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewMinEms(android.widget.RemoteViews, @IdRes int viewId, int minems);
+    method @RequiresApi(31) public static void setTextViewMinHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewMinHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int minHeight);
+    method @RequiresApi(31) public static void setTextViewMinHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewMinHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewMinLines(android.widget.RemoteViews, @IdRes int viewId, int minLines);
+    method @RequiresApi(31) public static void setTextViewMinWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewMinWidth(android.widget.RemoteViews, @IdRes int viewId, @Px int minWidth);
+    method @RequiresApi(31) public static void setTextViewMinWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewMinWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setTextViewPaintFlags(android.widget.RemoteViews, @IdRes int viewId, int flags);
+    method public static void setTextViewSelectAllOnFocus(android.widget.RemoteViews, @IdRes int viewId, boolean selectAllOnFocus);
+    method public static void setTextViewSingleLine(android.widget.RemoteViews, @IdRes int viewId, boolean singleLine);
+    method public static void setTextViewText(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(31) public static void setTextViewTextAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewTextColor(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList colors);
+    method @RequiresApi(31) public static void setTextViewTextColor(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList notNight, android.content.res.ColorStateList night);
+    method public static void setTextViewTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setTextViewTextColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setTextViewTextColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewTextColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setTextViewTextScaleX(android.widget.RemoteViews, @IdRes int viewId, float size);
+    method @RequiresApi(31) public static void setTextViewTextSizeDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewTextSizeDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setTextViewWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method public static void setTextViewWidth(android.widget.RemoteViews, @IdRes int viewId, @Px int pixels);
+    method @RequiresApi(31) public static void setTextViewWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setTextViewWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewAlpha(android.widget.RemoteViews, @IdRes int viewId, float alpha);
+    method public static void setViewBackgroundColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int color);
+    method @RequiresApi(31) public static void setViewBackgroundColor(android.widget.RemoteViews, @IdRes int viewId, @ColorInt int notNight, @ColorInt int night);
+    method @RequiresApi(31) public static void setViewBackgroundColorAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setViewBackgroundColorResource(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method public static void setViewBackgroundResource(android.widget.RemoteViews, @IdRes int viewId, @DrawableRes int resId);
+    method @RequiresApi(31) public static void setViewBackgroundTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setViewBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setViewBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setViewBackgroundTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setViewBackgroundTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewClipToOutline(android.widget.RemoteViews, @IdRes int viewId, boolean clipToOutline);
+    method @RequiresApi(31) public static void setViewContentDescription(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method public static void setViewContentDescription(android.widget.RemoteViews, @IdRes int viewId, CharSequence? contentDescription);
+    method @RequiresApi(31) public static void setViewContentDescriptionAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewElevationDimen(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewElevationDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewElevationDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(24) public static void setViewEnabled(android.widget.RemoteViews, @IdRes int viewId, boolean enabled);
+    method @RequiresApi(31) public static void setViewFocusable(android.widget.RemoteViews, @IdRes int viewId, boolean focusable);
+    method @RequiresApi(31) public static void setViewFocusable(android.widget.RemoteViews, @IdRes int viewId, int focusable);
+    method @RequiresApi(31) public static void setViewFocusableInTouchMode(android.widget.RemoteViews, @IdRes int viewId, boolean focusableInTouchMode);
+    method @RequiresApi(31) public static void setViewFocusedByDefault(android.widget.RemoteViews, @IdRes int viewId, boolean isFocusedByDefault);
+    method @RequiresApi(31) public static void setViewForegroundTintBlendMode(android.widget.RemoteViews, @IdRes int viewId, android.graphics.BlendMode? blendMode);
+    method @RequiresApi(31) public static void setViewForegroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? tint);
+    method @RequiresApi(31) public static void setViewForegroundTintList(android.widget.RemoteViews, @IdRes int viewId, android.content.res.ColorStateList? notNightTint, android.content.res.ColorStateList? nightTint);
+    method @RequiresApi(31) public static void setViewForegroundTintList(android.widget.RemoteViews, @IdRes int viewId, @ColorRes int resId);
+    method @RequiresApi(31) public static void setViewForegroundTintListAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setViewLayoutDirection(android.widget.RemoteViews, @IdRes int viewId, int layoutDirection);
+    method @RequiresApi(31) public static void setViewMinimumHeight(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(24) public static void setViewMinimumHeight(android.widget.RemoteViews, @IdRes int viewId, @Px int minHeight);
+    method @RequiresApi(31) public static void setViewMinimumHeightDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewMinimumHeightDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewMinimumWidth(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewMinimumWidthDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewMinimumWidthDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewPivotX(android.widget.RemoteViews, @IdRes int viewId, float pivotX);
+    method @RequiresApi(31) public static void setViewPivotY(android.widget.RemoteViews, @IdRes int viewId, float pivotY);
+    method @RequiresApi(31) public static void setViewRotation(android.widget.RemoteViews, @IdRes int viewId, float rotation);
+    method @RequiresApi(31) public static void setViewRotationX(android.widget.RemoteViews, @IdRes int viewId, float rotationX);
+    method @RequiresApi(31) public static void setViewRotationY(android.widget.RemoteViews, @IdRes int viewId, float rotationY);
+    method @RequiresApi(31) public static void setViewScaleX(android.widget.RemoteViews, @IdRes int viewId, float scaleX);
+    method @RequiresApi(31) public static void setViewScaleY(android.widget.RemoteViews, @IdRes int viewId, float scaleY);
+    method @RequiresApi(31) public static void setViewScrollIndicators(android.widget.RemoteViews, @IdRes int viewId, int scrollIndicators);
+    method @RequiresApi(31) public static void setViewStateDescription(android.widget.RemoteViews, @IdRes int viewId, @StringRes int resId);
+    method @RequiresApi(30) public static void setViewStateDescription(android.widget.RemoteViews, @IdRes int viewId, CharSequence? stateDescription);
+    method @RequiresApi(31) public static void setViewStateDescriptionAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method public static void setViewStubInflatedId(android.widget.RemoteViews, @IdRes int viewId, int inflatedId);
+    method public static void setViewStubLayoutResource(android.widget.RemoteViews, @IdRes int viewId, @LayoutRes int layoutResource);
+    method @RequiresApi(31) public static void setViewTranslationXDimen(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewTranslationXDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationXDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationYDimen(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewTranslationYDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationYDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationZDimen(android.widget.RemoteViews, @IdRes int viewId, float value, int unit);
+    method @RequiresApi(31) public static void setViewTranslationZDimen(android.widget.RemoteViews, @IdRes int viewId, @DimenRes int resId);
+    method @RequiresApi(31) public static void setViewTranslationZDimenAttr(android.widget.RemoteViews, @IdRes int viewId, @AttrRes int resId);
+    field public static final androidx.core.widget.RemoteViewsCompat INSTANCE;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems {
+    method public int getItemCount();
+    method public long getItemId(int position);
+    method public android.widget.RemoteViews getItemView(int position);
+    method public int getViewTypeCount();
+    method public boolean hasStableIds();
+    property public final int itemCount;
+    property public final int viewTypeCount;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems.Builder {
+    ctor public RemoteViewsCompat.RemoteCollectionItems.Builder();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder addItem(long id, android.widget.RemoteViews view);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems build();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setHasStableIds(boolean hasStableIds);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setViewTypeCount(int viewTypeCount);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class RemoteViewsCompatService extends android.widget.RemoteViewsService {
+    ctor public RemoteViewsCompatService();
+    method public android.widget.RemoteViewsService.RemoteViewsFactory onGetViewFactory(android.content.Intent intent);
+  }
+
+}
+
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
index 1e24dd7..7bbcabb 100644
--- a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
@@ -496,14 +496,6 @@
                 }
             }
 
-            if (theme.resolveAttribute(attr.enforceNavigationBarContrast, tv, true)) {
-                window.isNavigationBarContrastEnforced = tv.data != 0
-            }
-
-            if (theme.resolveAttribute(attr.enforceStatusBarContrast, tv, true)) {
-                window.isStatusBarContrastEnforced = tv.data != 0
-            }
-
             val decorView = window.decorView as ViewGroup
             ThemeUtils.Api31.applyThemesSystemBarAppearance(theme, decorView, tv)
 
diff --git a/core/core-splashscreen/src/main/res/values-v31/styles.xml b/core/core-splashscreen/src/main/res/values-v31/styles.xml
index 2e4d189..7c48243 100644
--- a/core/core-splashscreen/src/main/res/values-v31/styles.xml
+++ b/core/core-splashscreen/src/main/res/values-v31/styles.xml
@@ -15,6 +15,13 @@
   -->
 
 <resources>
+
+    <!-- For API 31+, reset enforceNavigationBarContrast to system default value, so it won't
+    corrupt activity's theme -->
+    <style name="Base.Theme.SplashScreen" parent="android:Theme.DeviceDefault.DayNight">
+        <item name="android:enforceNavigationBarContrast">true</item>
+    </style>
+
     <style name="Theme.SplashScreen" parent="Base.Theme.SplashScreen.DayNight">
         <item name="android:windowSplashScreenAnimatedIcon">?windowSplashScreenAnimatedIcon</item>
         <item name="android:windowSplashScreenBackground">?windowSplashScreenBackground</item>
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingParameters.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingParameters.kt
index 88efc1f..8a7c502 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingParameters.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingParameters.kt
@@ -115,7 +115,8 @@
          * deferred mode,
          * ranging interval = 240 ms,
          * slot duration = 2400 RSTU,
-         * slots per ranging round = 6
+         * slots per ranging round = 6,
+         * hopping mode is enabled
          *
          * All other MAC parameters use FiRa/UCI default values.
          *
@@ -129,7 +130,8 @@
          * deferred mode,
          * ranging interval = 200 ms,
          * slot duration = 2400 RSTU,
-         * slots per ranging round = 20
+         * slots per ranging round = 20,
+         * hopping mode is enabled
          *
          * All other MAC parameters use FiRa/UCI default values.
          *
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index ee07459..d1e3a3e 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -134,13 +134,13 @@
     docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha13")
     docs("androidx.contentpager:contentpager:1.0.0")
     docs("androidx.coordinatorlayout:coordinatorlayout:1.3.0-alpha02")
-    docs("androidx.core:core:1.13.0-rc01")
+    docs("androidx.core:core:1.13.0")
     // TODO(b/294531403): Turn on apiSince for core-animation when it releases as alpha
     docsWithoutApiSince("androidx.core:core-animation:1.0.0-rc01")
     docsWithoutApiSince("androidx.core:core-animation-testing:1.0.0-rc01")
     docs("androidx.core:core-google-shortcuts:1.2.0-alpha01")
     docs("androidx.core:core-i18n:1.0.0-alpha01")
-    docs("androidx.core:core-ktx:1.13.0-rc01")
+    docs("androidx.core:core-ktx:1.13.0")
     docs("androidx.core:core-location-altitude:1.0.0-alpha01")
     docs("androidx.core:core-performance:1.0.0")
     docs("androidx.core:core-performance-play-services:1.0.0")
@@ -150,7 +150,7 @@
     docs("androidx.core:core-role:1.2.0-alpha01")
     docs("androidx.core:core-splashscreen:1.1.0-alpha02")
     docs("androidx.core:core-telecom:1.0.0-alpha02")
-    docs("androidx.core:core-testing:1.13.0-rc01")
+    docs("androidx.core:core-testing:1.13.0")
     docs("androidx.core.uwb:uwb:1.0.0-alpha08")
     docs("androidx.core.uwb:uwb-rxjava3:1.0.0-alpha08")
     docs("androidx.credentials:credentials:1.3.0-alpha02")
diff --git a/fragment/integration-tests/testapp/build.gradle b/fragment/integration-tests/testapp/build.gradle
index 3bf95dc..a5151d3 100644
--- a/fragment/integration-tests/testapp/build.gradle
+++ b/fragment/integration-tests/testapp/build.gradle
@@ -32,7 +32,6 @@
     debugImplementation(project(":fragment:fragment-testing-manifest"))
 
     androidTestImplementation(project(":fragment:fragment-testing"))
-    androidTestImplementation(project(":core:core-ktx"))
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.truth)
diff --git a/libraryversions.toml b/libraryversions.toml
index 8c19ba3..682c409 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,6 +1,6 @@
 [versions]
 ACTIVITY = "1.10.0-alpha01"
-ANNOTATION = "1.8.0-beta01"
+ANNOTATION = "1.8.0-beta02"
 ANNOTATION_EXPERIMENTAL = "1.4.0-rc01"
 APPACTIONS_BUILTINTYPES = "1.0.0-alpha01"
 APPACTIONS_INTERACTION = "1.0.0-alpha01"
@@ -22,11 +22,11 @@
 CARDVIEW = "1.1.0-alpha01"
 CAR_APP = "1.7.0-alpha02"
 COLLECTION = "1.5.0-alpha01"
-COMPOSE = "1.7.0-alpha06"
+COMPOSE = "1.7.0-alpha07"
 COMPOSE_COMPILER = "1.5.11"  # Update when preparing for a release
-COMPOSE_MATERIAL3 = "1.3.0-alpha04"
-COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha10"
-COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha05"
+COMPOSE_MATERIAL3 = "1.3.0-alpha05"
+COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha11"
+COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha06"
 COMPOSE_MATERIAL3_COMMON = "1.0.0-alpha01"
 COMPOSE_RUNTIME_TRACING = "1.0.0-beta01"
 CONSTRAINTLAYOUT = "2.2.0-alpha13"
@@ -43,7 +43,7 @@
 CORE_I18N = "1.0.0-alpha02"
 CORE_LOCATION_ALTITUDE = "1.0.0-alpha02"
 CORE_PERFORMANCE = "1.0.0"
-CORE_REMOTEVIEWS = "1.1.0-beta01"
+CORE_REMOTEVIEWS = "1.1.0-beta02"
 CORE_ROLE = "1.2.0-alpha01"
 CORE_SPLASHSCREEN = "1.1.0-alpha02"
 CORE_TELECOM = "1.0.0-alpha03"
@@ -101,7 +101,7 @@
 MEDIAROUTER = "1.7.0-rc01"
 METRICS = "1.0.0-beta02"
 NAVIGATION = "2.8.0-alpha07"
-PAGING = "3.3.0-beta02"
+PAGING = "3.3.0-rc01"
 PALETTE = "1.1.0-alpha01"
 PDF = "1.0.0-alpha01"
 PERCENTLAYOUT = "1.1.0-alpha01"
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigatorDestinationBuilder.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigatorDestinationBuilder.kt
index a479847..104549d 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigatorDestinationBuilder.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigatorDestinationBuilder.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:SuppressLint("NullAnnotationGroup") // b/331484152
-
 package androidx.navigation.compose
 
-import android.annotation.SuppressLint
 import androidx.compose.animation.AnimatedContentScope
 import androidx.compose.animation.AnimatedContentTransitionScope
 import androidx.compose.animation.EnterTransition
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigatorDestinationBuilder.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigatorDestinationBuilder.kt
index 2fc804b..3ef68e1 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigatorDestinationBuilder.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigatorDestinationBuilder.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:SuppressLint("NullAnnotationGroup") // b/331484152
-
 package androidx.navigation.compose
 
-import android.annotation.SuppressLint
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.window.DialogProperties
 import androidx.navigation.ExperimentalSafeArgsApi
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
index fce08ae..ebfe8cc 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:SuppressLint("NullAnnotationGroup") // b/331484152
-
 package androidx.navigation.compose
 
-import android.annotation.SuppressLint
 import androidx.annotation.RestrictTo
 import androidx.compose.animation.AnimatedContentScope
 import androidx.compose.animation.AnimatedContentTransitionScope
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
index b4ea8f5..0e8aec0 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:SuppressLint("NullAnnotationGroup") // b/331484152
-
 package androidx.navigation.compose
 
 import android.annotation.SuppressLint
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigatorDestinationBuilder.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigatorDestinationBuilder.kt
index 829413f..f4a750e 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigatorDestinationBuilder.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigatorDestinationBuilder.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:SuppressLint("NullAnnotationGroup") // b/331484152
-
 package androidx.navigation.fragment
 
-import android.annotation.SuppressLint
 import androidx.annotation.IdRes
 import androidx.fragment.app.DialogFragment
 import androidx.navigation.ExperimentalSafeArgsApi
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt
index 6775a01..4b2e036 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:SuppressLint("NullAnnotationGroup") // b/331484152
-
 package androidx.navigation.fragment
 
-import android.annotation.SuppressLint
 import androidx.annotation.IdRes
 import androidx.fragment.app.Fragment
 import androidx.navigation.ExperimentalSafeArgsApi
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt
index c0ac9e2..7554bd1 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt
@@ -15,11 +15,9 @@
  */
 
 @file:Suppress("NOTHING_TO_INLINE")
-@file:SuppressLint("NullAnnotationGroup") // b/331484152
 
 package androidx.navigation
 
-import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.ComponentName
 import android.content.Context
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index c70ce05..ebc2ab6 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:SuppressLint("NullAnnotationGroup")
-
 package androidx.navigation
 
-import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.Context
 import android.content.ContextWrapper
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHost.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHost.kt
index a739018..dbe0634 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHost.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHost.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:SuppressLint("NullAnnotationGroup")
-
 package androidx.navigation
 
-import android.annotation.SuppressLint
 import androidx.annotation.IdRes
 import kotlin.reflect.KClass
 import kotlin.reflect.KType
diff --git a/pdf/pdf-viewer/lint-baseline.xml b/pdf/pdf-viewer/lint-baseline.xml
index 640c026..509bcca 100644
--- a/pdf/pdf-viewer/lint-baseline.xml
+++ b/pdf/pdf-viewer/lint-baseline.xml
@@ -7,6 +7,15 @@
         errorLine1="import java.util.concurrent.ConcurrentHashMap;"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
+            file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+    </issue>
+
+    <issue
+        id="BanConcurrentHashMap"
+        message="Detected ConcurrentHashMap usage."
+        errorLine1="import java.util.concurrent.ConcurrentHashMap;"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
             file="src/main/java/androidx/pdf/util/Timer.java"/>
     </issue>
 
@@ -110,6 +119,15 @@
     </issue>
 
     <issue
+        id="AppCompatCustomView"
+        message="This custom view should extend `androidx.appcompat.widget.AppCompatEditText` instead"
+        errorLine1="public class SearchEditText extends EditText {"
+        errorLine2="                                    ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+    </issue>
+
+    <issue
         id="ObsoleteSdkInt"
         message="Unnecessary; SDK_INT is always >= 30"
         errorLine1="        if (VERSION.SDK_INT >= VERSION_CODES.Q) {"
@@ -796,6 +814,33 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public String getCachedMimeType(Uri uri) {"
+        errorLine2="                                    ~~~">
+        <location
+            file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static File getLongTermCacheDir(Context context) {"
+        errorLine2="                  ~~~~">
+        <location
+            file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static File getLongTermCacheDir(Context context) {"
+        errorLine2="                                           ~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public Uri getUri() {"
         errorLine2="           ~~~">
         <location
@@ -1066,6 +1111,132 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static void open(String url, Activity activity) {"
+        errorLine2="                            ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static void open(String url, Activity activity) {"
+        errorLine2="                                        ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static void open(Uri uri, Activity activity) {"
+        errorLine2="                            ~~~">
+        <location
+            file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static void open(Uri uri, Activity activity) {"
+        errorLine2="                                     ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static String getDescription(String url, Context context) {"
+        errorLine2="                  ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static String getDescription(String url, Context context) {"
+        errorLine2="                                        ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static String getDescription(String url, Context context) {"
+        errorLine2="                                                    ~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    void setFastScrollListener(FastScrollListener listener);"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/FastScrollContentModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public FastScrollView(Context context, AttributeSet attrs) {"
+        errorLine2="                          ~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public FastScrollView(Context context, AttributeSet attrs) {"
+        errorLine2="                                           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public FastScrollView(Context context, AttributeSet attrs, int defStyle) {"
+        errorLine2="                          ~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public FastScrollView(Context context, AttributeSet attrs, int defStyle) {"
+        errorLine2="                                           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void setScrollable(FastScrollContentModel scrollable) {"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public ObservableValue&lt;Integer> getScrollerPositionY() {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public static Fetcher build(Context context) {"
         errorLine2="                  ~~~~~~~">
         <location
@@ -1300,6 +1471,24 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    boolean onQueryTextChange(String query);"
+        errorLine2="                              ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/find/FindInFileListener.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    boolean onFindNextMatch(String query, boolean backwards);"
+        errorLine2="                            ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/find/FindInFileListener.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    void get(Callback&lt;T> callback);"
         errorLine2="             ~~~~~~~~~~~">
         <location
@@ -2731,6 +2920,15 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public abstract void sendPassword(EditText textField);"
+        errorLine2="                                      ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/password/PasswordDialog.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public void setOnConnectInitializer(Runnable onConnect) {"
         errorLine2="                                        ~~~~~~~~">
         <location
@@ -3028,6 +3226,78 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void sendPassword(EditText textField) {"
+        errorLine2="                             ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfPasswordDialog.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="            PdfSelectionModel selectionModel, ZoomView zoomView, PaginatedView pdfView) {"
+        errorLine2="            ~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="            PdfSelectionModel selectionModel, ZoomView zoomView, PaginatedView pdfView) {"
+        errorLine2="                                              ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="            PdfSelectionModel selectionModel, ZoomView zoomView, PaginatedView pdfView) {"
+        errorLine2="                                                                 ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public PdfSelectionModel(PdfLoader pdfLoader) {"
+        errorLine2="                             ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfSelectionModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public String getText() {"
+        errorLine2="           ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfSelectionModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {"
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfSelectionModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {"
+        errorLine2="                                                              ~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfSelectionModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public void schedule(AbstractPdfTask&lt;?> task) {"
         errorLine2="                         ~~~~~~~~~~~~~~~~~~">
         <location
@@ -3037,6 +3307,123 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    protected String getLogTag() {"
+        errorLine2="              ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public PdfViewer setQuitOnError(boolean quit) {"
+        errorLine2="           ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public PdfViewer setExitOnPasswordCancel(boolean shouldExitOnPasswordCancel) {"
+        errorLine2="           ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void onCreate(Bundle savedInstanceState) {"
+        errorLine2="                         ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+        errorLine2="           ~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+        errorLine2="                             ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+        errorLine2="                                                      ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+        errorLine2="                                                                           ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void onActivityCreated(Bundle savedInstanceState) {"
+        errorLine2="                                  ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    protected void onContentsAvailable(DisplayData contents, @Nullable Bundle savedState) {"
+        errorLine2="                                       ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void setPassword(String password) {"
+        errorLine2="                            ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void onSaveInstanceState(Bundle outState) {"
+        errorLine2="                                    ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void setFastScrollListener(final FastScrollListener listener) {"
+        errorLine2="                                            ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public static void checkState(boolean state, String message) {"
         errorLine2="                                                 ~~~~~~">
         <location
@@ -3334,6 +3721,150 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public SearchEditText(Context context) {"
+        errorLine2="                          ~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public SearchEditText(Context context, AttributeSet attrs) {"
+        errorLine2="                          ~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public SearchEditText(Context context, AttributeSet attrs) {"
+        errorLine2="                                           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public SearchEditText(Context context, AttributeSet attrs, int defStyle) {"
+        errorLine2="                          ~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public SearchEditText(Context context, AttributeSet attrs, int defStyle) {"
+        errorLine2="                                           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {"
+        errorLine2="                                                                  ~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public SearchModel(PdfLoader pdfLoader) {"
+        errorLine2="                       ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public ObservableValue&lt;String> query() {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public ObservableValue&lt;SelectedMatch> selectedMatch() {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public ObservableValue&lt;MatchCount> matchCount() {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public boolean updateMatches(String matchesQuery, int page, MatchRects matches) {"
+        errorLine2="                                 ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public boolean updateMatches(String matchesQuery, int page, MatchRects matches) {"
+        errorLine2="                                                                ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void selectNextMatch(Direction direction, int viewingPage) {"
+        errorLine2="                                ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public PdfHighlightOverlay getOverlay(String matchesQuery, int page, MatchRects matches) {"
+        errorLine2="                                          ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public PdfHighlightOverlay getOverlay(String matchesQuery, int page, MatchRects matches) {"
+        errorLine2="                                                                         ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public static String whiteSpaceToNull(String query) {"
+        errorLine2="                                          ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public static SelectionBoundary atIndex(int index) {"
         errorLine2="                  ~~~~~~~~~~~~~~~~~">
         <location
@@ -3379,6 +3910,42 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public ObservableValue&lt;S> selection() {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/select/SelectionModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public abstract String getText();"
+        errorLine2="                    ~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/select/SelectionModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {"
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/select/SelectionModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {"
+        errorLine2="                                                              ~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/select/SelectionModel.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    T supply(Progress progress) throws Exception;"
         errorLine2="             ~~~~~~~~">
         <location
@@ -3445,7 +4012,7 @@
         errorLine1="    public TaskCancelledException(String detailMessage) {"
         errorLine2="                                  ~~~~~~">
         <location
-            file="src/main/java/androidx/pdf/fetcher/TaskCancelledException.java"/>
+            file="src/main/java/androidx/pdf/exceptions/TaskCancelledException.java"/>
     </issue>
 
     <issue
@@ -3739,6 +4306,15 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    public TileInfo getTileInfo() {"
+        errorLine2="           ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/pdf/widget/TileView.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    protected void onDraw(Canvas canvas) {"
         errorLine2="                          ~~~~~~">
         <location
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Range.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Range.java
index bba8bbb2..5bed535 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Range.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Range.java
@@ -26,8 +26,8 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 public final class Range implements Iterable<Integer> {
-    public final int mFirst;
-    public final int mLast;
+    private final int mFirst;
+    private final int mLast;
 
     public Range(int first, int last) {
         this.mFirst = first;
@@ -38,6 +38,14 @@
         this(0, -1);
     }
 
+    public int getFirst() {
+        return mFirst;
+    }
+
+    public int getLast() {
+        return mLast;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/UiFutureValues.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/UiFutureValues.java
index d5ffbad..bd13cc8 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/UiFutureValues.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/UiFutureValues.java
@@ -57,7 +57,6 @@
  * value that was passed to it (an actual value or an Exception).
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressWarnings("deprecation")
 public class UiFutureValues {
     private static final String TAG = UiFutureValues.class.getSimpleName();
     private static final Executor DEFAULT_EXECUTOR = Executors.newFixedThreadPool(4,
@@ -164,8 +163,9 @@
     }
 
     /**
-     * A {@link FutureValue.Callback} wrapper interface around another {@link FutureValue.Callback}
-     * that ensures that each callback call is run on the UI thread.
+     * A {@link FutureValue.Callback} wrapper interface around another
+     * {@link FutureValue.Callback} that ensures that each
+     * callback call is run on the UI thread.
      */
     private static <T> FutureValue.Callback<T> runOnUi(
             final FutureValue.Callback<T> targetCallback) {
@@ -233,6 +233,7 @@
      *
      * @param <T> The type of the value being supplied.
      */
+    @SuppressWarnings("deprecation")
     private static class FutureAsyncTask<T> extends android.os.AsyncTask<Void, Float, T> {
 
         private final Supplier<T> mSupplier;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/fetcher/TaskCancelledException.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/exceptions/TaskCancelledException.java
similarity index 97%
rename from pdf/pdf-viewer/src/main/java/androidx/pdf/fetcher/TaskCancelledException.java
rename to pdf/pdf-viewer/src/main/java/androidx/pdf/exceptions/TaskCancelledException.java
index c3ea492..49d2ca1 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/fetcher/TaskCancelledException.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/exceptions/TaskCancelledException.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.pdf.fetcher;
+package androidx.pdf.exceptions;
 
 import androidx.annotation.RestrictTo;
 
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/fetcher/DiskCache.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/fetcher/DiskCache.java
index 8e68616..1d869d8 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/fetcher/DiskCache.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/fetcher/DiskCache.java
@@ -22,14 +22,22 @@
 import androidx.annotation.RestrictTo;
 
 import java.io.File;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nullable;
 
 /**
  * A very simple disk cache that caches file contents using an {@link Uri} and mimeType as key.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 public class DiskCache {
+
     private static final String TAG = DiskCache.class.getSimpleName();
 
+    /** The folder where long term cache lives, in the app's cache directory. */
+    private static final String LONG_TERM_CACHE_DIR = "projector";
+
     /** The root folder where this cache lives, in the app's cache directory. */
     private static final String SUB_CACHE_DIR = "projector-disk";
 
@@ -38,6 +46,11 @@
 
     private final File mCacheRoot;
     private final File mTmpCacheRoot;
+
+    /** Catalog of entries in this cache, with their mime types. */
+    // TODO: This should be persisted together with the cached files.
+    private final Map<Uri, String> mEntries = new ConcurrentHashMap<>();
+
     public DiskCache(Context context) {
         mCacheRoot = getDiskCacheDir(context);
         mTmpCacheRoot = getTmpCacheDir(context);
@@ -45,6 +58,39 @@
         mTmpCacheRoot.mkdir();
     }
 
+    /** Delete the contents of the cache directories, without deleting the actual directories. */
+    public void cleanup() {
+        clearDirectory(mCacheRoot);
+        clearDirectory(mTmpCacheRoot);
+        mEntries.clear();
+    }
+
+    /** Returns the cached MimeType of this Uri. */
+    @Nullable
+    public String getCachedMimeType(Uri uri) {
+        // TODO: this can often be null, since entries are not persisted to disk.
+        return mEntries.get(uri);
+    }
+
+    private void clearDirectory(File directory) {
+        if (directory.exists()) {
+            File[] files = directory.listFiles();
+            for (int i = 0; i < files.length; ++i) {
+                files[i].delete();
+            }
+        }
+    }
+
+    /**
+     * Returns a directory for a long term un-managed cache.
+     *
+     * <p>This dir is not guaranteed to exist. Users of this directory need to ensure that they
+     * clean up their data overtime.
+     */
+    public static File getLongTermCacheDir(Context context) {
+        return new File(context.getCacheDir(), LONG_TERM_CACHE_DIR);
+    }
+
     // TODO: Make this private. Currently used by FileProvider to access a cached file.
     static File getDiskCacheDir(Context context) {
         return new File(context.getCacheDir(), SUB_CACHE_DIR);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileListener.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileListener.java
new file mode 100644
index 0000000..2b4eb77
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileListener.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.find;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.util.ObservableValue;
+
+/**
+ * Callback interface for listening to user actions to find text in a file.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface FindInFileListener {
+
+    /**
+     * Called when the query text is changed by the user.
+     *
+     * @param query The text the user is searching for.
+     */
+    boolean onQueryTextChange(String query);
+
+    /**
+     * The user is attempting to find the next match of the query text.
+     *
+     * @param query     The text the user is searching for.
+     * @param backwards True iff the user is searching for the previous match.
+     */
+    boolean onFindNextMatch(String query, boolean backwards);
+
+    /**
+     * Get an ObservableValue that changes whenever MatchCount data is changed -
+     * when more matches are found or the selected match is changed.
+     * Can be null if not supported, or if the document is not ready or is destroyed.
+     */
+    @Nullable
+    ObservableValue<MatchCount> matchCount();
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/MatchCount.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/MatchCount.java
new file mode 100644
index 0000000..9cba198
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/MatchCount.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.find;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Information about which match is selected and how many matches are found.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class MatchCount {
+    public final int mSelectedIndex;  // Zero-based, -1 means no selected match.
+    public final int mTotalMatches;
+    public final boolean mIsAllPagesCounted;
+
+    public MatchCount(int selectedIndex, int totalMatches, boolean isAllPagesCounted) {
+        this.mSelectedIndex = selectedIndex;
+        this.mTotalMatches = totalMatches;
+        this.mIsAllPagesCounted = isAllPagesCounted;
+    }
+
+    @Override
+    public int hashCode() {
+        return (mSelectedIndex + 31 * mTotalMatches) * (mIsAllPagesCounted ? 1 : -1);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof MatchCount)) {
+            return false;
+        }
+        MatchCount that = (MatchCount) other;
+        return this.mSelectedIndex == that.mSelectedIndex
+                && this.mTotalMatches == that.mTotalMatches
+                && this.mIsAllPagesCounted == that.mIsAllPagesCounted;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("MatchCount(%d of %d, allPagesCounted=%s)",
+                mSelectedIndex, mTotalMatches, mIsAllPagesCounted);
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionModel.java
new file mode 100644
index 0000000..5f8428b
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionModel.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.select;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.aidl.SelectionBoundary;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.Observables;
+import androidx.pdf.util.Observables.ExposedValue;
+
+/**
+ * Stores data relevant to the current selection.
+ *
+ * @param <S> Type for the model
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public abstract class SelectionModel<S> {
+
+    protected final ExposedValue<S> mSelection =
+            Observables.newExposedValueWithInitialValue(null);
+
+    /**
+     *
+     */
+    public ObservableValue<S> selection() {
+        return mSelection;
+    }
+
+    /**
+     *
+     */
+    public abstract String getText();
+
+    /** Synchronous update - the exact selection is already known. */
+    public void setSelection(S newSelection) {
+        mSelection.set(newSelection);
+    }
+
+    /**
+     *
+     */
+    public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {
+        throw new UnsupportedOperationException("No support for updating selection");
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ExternalLinks.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ExternalLinks.java
new file mode 100644
index 0000000..6f123f4
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ExternalLinks.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.R;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utilities for describing and opening external links.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public final class ExternalLinks {
+    private static final String TAG = "ExternalLinks";
+
+    // Allow to open any of the allow-listed URL schemes
+    private static final Set<String> ALLOWED_SCHEMES = new HashSet<String>();
+
+    static {
+        ALLOWED_SCHEMES.add("http");
+        ALLOWED_SCHEMES.add("https");
+        ALLOWED_SCHEMES.add("mailto");
+        ALLOWED_SCHEMES.add("tel");
+    }
+
+    private static final int SHORTEN_LENGTH = 40;
+
+    /** Open the given link in a browser or similar, if it is safe to do so. */
+    public static void open(String url, Activity activity) {
+        open(Uri.parse(url), activity);
+    }
+
+    /** Open the given URI. */
+    public static void open(Uri uri, Activity activity) {
+        if (TextUtils.isEmpty(uri.getScheme())) {
+            uri = uri.buildUpon().scheme("http").build();
+        }
+        if (ALLOWED_SCHEMES.contains(uri.getScheme())) {
+            PackageManager pm = activity.getPackageManager();
+            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+            if (!pm.queryIntentActivities(intent, 0).isEmpty()) {
+                Intents.startActivity(activity, TAG, intent);
+            }
+            // TODO: Track hyperlink click.
+        }
+    }
+
+    /**
+     * A short description of an external link suitable for reading aloud - eg
+     * {@code Link: www.example.com/page1.html} or
+     * {@code Link: webpage at www.example.com}
+     */
+    public static String getDescription(String url, Context context) {
+        return getDescription(Uri.parse(url), context);
+    }
+
+    private static String getDescription(Uri uri, Context context) {
+        if (TextUtils.isEmpty(uri.getScheme())) {
+            uri = uri.buildUpon().scheme("http").build();
+        }
+        String scheme = uri.getScheme();
+        boolean isWebUri = "http".equals(scheme) || "https".equals(scheme);
+        String nonScheme = uri.getSchemeSpecificPart().replaceFirst("^//", "");
+        if (nonScheme == null) {  // URL must be bad, but we'll try to describe it anyway:
+            return context.getString(R.string.desc_web_link, uri.toString());
+        }
+
+        String host = uri.getHost();
+        if (nonScheme.length() > SHORTEN_LENGTH && isWebUri
+                && host != null && host.length() < nonScheme.length()) {
+            // Don't describe the entire URL, just say "webpage at [domain]"
+            return context.getString(R.string.desc_web_link_shortened_to_domain, host);
+        } else if (isWebUri) {
+            return context.getString(R.string.desc_web_link, nonScheme);
+        } else if ("mailto".equals(scheme)) {
+            return context.getString(R.string.desc_email_link, nonScheme);
+        } else if ("tel".equals(scheme)) {
+            return context.getString(R.string.desc_phone_link, nonScheme);
+        }
+        return context.getString(R.string.desc_web_link, uri.toString());
+    }
+
+    private ExternalLinks() {
+        // Static utility.
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/GestureTracker.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/GestureTracker.java
index 9cd1ed5..52724d23 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/GestureTracker.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/GestureTracker.java
@@ -421,8 +421,9 @@
 
     private void log(String msg) {
         Log.v(TAG,
-                String.format("[%s] msg: %s %s (%s) [Handling: %s]", mViewTag, msg,
-                        mDetectedGesture, mLog, mHandling));
+                String.format("[%s] %s %s (%s) [Handling: %s]", mViewTag, msg, mDetectedGesture,
+                        mLog,
+                        mHandling));
     }
 
     /** A recipient for all gesture handling. */
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ProjectorContext.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ProjectorContext.java
index 4e5a8a2..92f27a7 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ProjectorContext.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ProjectorContext.java
@@ -17,6 +17,7 @@
 package androidx.pdf.util;
 
 import android.content.Context;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
@@ -31,7 +32,6 @@
     private static ProjectorContext sInstance;
 
     private final ProjectorGlobals mGlobals;
-    private final Context mCurrentAppContext;
 
     /**
      *
@@ -52,7 +52,7 @@
     }
 
     private ProjectorContext(Context appContext, Screen screen) {
-        mCurrentAppContext = appContext;
+        Log.d("ProjectorContext", String.format("appContext: %s", appContext));
         mGlobals = new ProjectorGlobals(screen);
     }
 
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageIndicator.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageIndicator.java
index 7afd9e9..210a15e 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageIndicator.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageIndicator.java
@@ -117,11 +117,12 @@
         Resources res = mContext.getResources();
         switch (range.length()) {
             case 0:
-                return res.getString(R.string.label_page_single, range.mLast + 1, mNumPages);
+                return res.getString(R.string.label_page_single, range.getLast() + 1, mNumPages);
             case 1:
-                return res.getString(R.string.label_page_single, range.mFirst + 1, mNumPages);
+                return res.getString(R.string.label_page_single, range.getFirst() + 1, mNumPages);
             default:
-                return res.getString(R.string.label_page_range, range.mFirst + 1, range.mLast + 1,
+                return res.getString(R.string.label_page_range, range.getFirst() + 1,
+                        range.getLast() + 1,
                         mNumPages);
         }
     }
@@ -130,11 +131,12 @@
         Resources res = mContext.getResources();
         switch (range.length()) {
             case 0:
-                return res.getString(R.string.desc_page_single, range.mLast + 1, mNumPages);
+                return res.getString(R.string.desc_page_single, range.getLast() + 1, mNumPages);
             case 1:
-                return res.getString(R.string.desc_page_single, range.mFirst + 1, mNumPages);
+                return res.getString(R.string.desc_page_single, range.getFirst() + 1, mNumPages);
             default:
-                return res.getString(R.string.desc_page_range, range.mFirst + 1, range.mLast + 1,
+                return res.getString(R.string.desc_page_range, range.getFirst() + 1,
+                        range.getLast() + 1,
                         mNumPages);
         }
     }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageLinksView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageLinksView.java
index 2ce2c37..f74d742 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageLinksView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageLinksView.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -43,7 +44,7 @@
  * children.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressWarnings({"deprecation", "UnusedVariable", "NarrowingCompoundAssignment"})
+@SuppressWarnings("deprecation")
 public class PageLinksView extends LinearLayout {
     private static final String TAG = PageLinksView.class.getSimpleName();
 
@@ -51,7 +52,6 @@
 
     @Nullable
     private LinkRects mUrlLinks;
-    private final ObservableValue<ZoomView.ZoomScroll> mZoomScroll;
     private ExploreByTouchHelper mTouchHelper;
 
     public PageLinksView(Context context, ObservableValue<ZoomView.ZoomScroll> zoomScroll) {
@@ -59,7 +59,6 @@
         setLayoutParams(
                 new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                         ViewGroup.LayoutParams.MATCH_PARENT));
-        this.mZoomScroll = zoomScroll;
         setWillNotDraw(true);
         setFocusableInTouchMode(false);
     }
@@ -121,7 +120,6 @@
 
         @Override
         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
-            int linkSize = mUrlLinks != null ? mUrlLinks.size() : 0;
             if (mUrlLinks != null) {
                 for (int i = 0; i < mUrlLinks.size(); i++) {
                     virtualViewIds.add(i);
@@ -160,24 +158,10 @@
 
             node.setContentDescription(getContentDescription(virtualViewId));
             node.setFocusable(true);
-
-            Rect bounds = null;
-
-            if (bounds != null) {
-                // The AccessibilityNodeInfo isn't automatically scaled by the scaling of the View
-                // it is part of, so we have to do that ourselves - in contrast to
-                // #getVirtualViewAt.
-                float zoom = mZoomScroll.get().zoom;
-                bounds.top *= zoom;
-                bounds.bottom *= zoom;
-                bounds.left *= zoom;
-                bounds.right *= zoom;
-
-                node.setBoundsInParent(bounds);
-            }
         }
 
         private boolean isLinkLoaded(int virtualViewId) {
+            Log.d(TAG, String.format("virtualViewId %d", virtualViewId));
             // Links can be deleted as we unload pages as the user scrolls around - if this
             // happens but an event for the link somehow happens afterward, we should ignore it
             // and try not to crash. Also, the accessibility framework sometimes requests links
@@ -186,6 +170,7 @@
         }
 
         private String getContentDescription(int virtualViewId) {
+            Log.d(TAG, String.format("virtualViewId %d", virtualViewId));
             // TODO: Uncomment after resolving gotopagelinks
             return "";
         }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
index 5f4626d..fe5a196 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
@@ -37,10 +37,8 @@
 public class PageMosaicView extends MosaicView implements PageViewFactory.PageView {
 
     private static final String SEARCH_OVERLAY_KEY = "SearchOverlayKey";
-    private static final String COMMENT_ANCHOR_OVERLAY_KEY = "PdfCommentAnchorOverlayKey";
 
     private final int mPageNum;
-    private final Dimensions mPageSize;
     private String mPageText;
     private LinkRects mUrlLinks;
 
@@ -52,7 +50,6 @@
             BitmapRecycler bitmapRecycler) {
         super(context);
         this.mPageNum = pageNum;
-        this.mPageSize = pageSize;
         init(pageSize, bitmapRecycler, bitmapSource);
         setId(pageNum);
         setPageText(null);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
index 91b6580..ba67ec2 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
@@ -250,21 +250,21 @@
      * @return the range of visible pages (may be an empty range).
      */
     public Range getPagesInWindow(Range intervalPx, boolean includePartial) {
-        if (intervalPx.mFirst > mPageBottoms.get(mSize - 1)) {
+        if (intervalPx.getFirst() > mPageBottoms.get(mSize - 1)) {
             return new Range(mSize + 1, mSize);
         }
         List<Integer> startList = includePartial ? mPageBottoms : mPageTops;
         List<Integer> endList = includePartial ? mPageTops : mPageBottoms;
 
-        int topResult = Collections.binarySearch(startList, intervalPx.mFirst);
+        int topResult = Collections.binarySearch(startList, intervalPx.getFirst());
         int rangeStart = Math.abs(topResult + 1); // Insertion point.
 
-        int bottomResult = Collections.binarySearch(endList, intervalPx.mLast);
+        int bottomResult = Collections.binarySearch(endList, intervalPx.getLast());
         int rangeEnd = Math.abs(bottomResult + 1) - 1; // Before insertion point.
 
         if (rangeEnd < rangeStart) {
             // No page is entirely visible.
-            int midPoint = (intervalPx.mFirst + intervalPx.mLast) / 2;
+            int midPoint = (intervalPx.getFirst() + intervalPx.getLast()) / 2;
             int midResult = Collections.binarySearch(mPageTops, midPoint);
             int page = Math.max(Math.abs(midResult + 1) - 1, 0); // Before insertion point.
             return new Range(page, page);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfPasswordDialog.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfPasswordDialog.java
new file mode 100644
index 0000000..0291815
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfPasswordDialog.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer;
+
+import android.widget.EditText;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.viewer.password.PasswordDialog;
+
+/**
+ * This instance requires a {@link #getTargetFragment} to be set to give back the typed password.
+ * Currently, this target Fragment must be a {@link PdfViewer}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings("deprecation")
+public class PdfPasswordDialog extends PasswordDialog {
+
+    @Override
+    public void sendPassword(EditText textField) {
+        ((PdfViewer) getTargetFragment()).setPassword(textField.getText().toString());
+    }
+
+    @Override
+    public void showErrorOnDialogCancel() {
+        ((PdfViewer) getTargetFragment()).setPasswordCancelError();
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java
new file mode 100644
index 0000000..e6d93f3
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.R;
+import androidx.pdf.aidl.PageSelection;
+import androidx.pdf.aidl.SelectionBoundary;
+import androidx.pdf.select.SelectionModel;
+import androidx.pdf.util.Preconditions;
+import androidx.pdf.widget.ZoomView;
+import androidx.pdf.widget.ZoomableSelectionHandles;
+
+/**
+ * Implementation of SelectionHandles for PdfViewer.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PdfSelectionHandles extends ZoomableSelectionHandles<PageSelection> {
+
+    private final SelectionModel<?> mSelectionModel;
+    private final PaginatedView mPdfView;
+
+    private SelectionBoundary mFixed;
+    private SelectionBoundary mDragging;
+
+    public PdfSelectionHandles(
+            PdfSelectionModel selectionModel, ZoomView zoomView, PaginatedView pdfView) {
+        super(
+                zoomView, (ViewGroup) zoomView.findViewById(R.id.zoomed_view),
+                selectionModel.selection());
+        this.mSelectionModel = Preconditions.checkNotNull(selectionModel);
+        this.mPdfView = Preconditions.checkNotNull(pdfView);
+    }
+
+    @Override
+    protected void updateHandles() {
+        if (mSelection == null || mPdfView.getViewAt(mSelection.getPage()) == null) {
+            hideHandles();
+        } else {
+            View pageView = mPdfView.getViewAt(mSelection.getPage()).asView();
+            showHandle(mStartHandle, pageView, mSelection.getStart(), false);
+            showHandle(mStopHandle, pageView, mSelection.getStop(), true);
+        }
+    }
+
+    private void showHandle(
+            ImageView handle, View pageView, SelectionBoundary boundary, boolean isStop) {
+        float rawX = pageView.getX() + boundary.getX();
+        float rawY = pageView.getY() + boundary.getY();
+        boolean isRight = isStop ^ boundary.isRtl();
+        super.showHandle(handle, rawX, rawY, isRight);
+    }
+
+    @Override
+    protected void onDragHandleDown(boolean isStopHandle) {
+        mDragging = isStopHandle ? mSelection.getStop() : mSelection.getStart();
+        mFixed = isStopHandle ? mSelection.getStart() : mSelection.getStop();
+    }
+
+    @Override
+    protected void onDragHandleMove(int deltaX, int deltaY) {
+        SelectionBoundary updated = SelectionBoundary.atPoint(mDragging.getX() + deltaX,
+                mDragging.getY() + deltaY);
+        mSelectionModel.updateSelectionAsync(mFixed, updated);
+    }
+
+    @Override
+    protected void onDragHandleUp() {
+        // Nothing required.
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionModel.java
new file mode 100644
index 0000000..c83cbb8c
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionModel.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.aidl.PageSelection;
+import androidx.pdf.aidl.SelectionBoundary;
+import androidx.pdf.select.SelectionModel;
+import androidx.pdf.viewer.loader.PdfLoader;
+
+/**
+ * Selection model for pdfs.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PdfSelectionModel extends SelectionModel<PageSelection> {
+
+    private final PdfLoader mPdfLoader;
+
+    public PdfSelectionModel(PdfLoader pdfLoader) {
+        this.mPdfLoader = pdfLoader;
+    }
+
+    /** Return the page the selection is on. */
+    public int getPage() {
+        PageSelection value = mSelection.get();
+        return (value != null) ? value.getPage() : -1;
+    }
+
+    @Override
+    public String getText() {
+        PageSelection value = mSelection.get();
+        return (value != null) ? value.getText() : "";
+    }
+
+    /**
+     * Asynchronous update - the exact selection is not yet known, we need to make an async call to
+     * pdfClient to find out the exact selection. This will eventually cause
+     * {@link #setSelection} to be called.
+     */
+    @Override
+    public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {
+        if (mPdfLoader != null) {
+            int page = Math.max(0, getPage());
+            mPdfLoader.selectPageText(page, start, stop);
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
new file mode 100644
index 0000000..cb320f0
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
@@ -0,0 +1,1396 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.pdf.R;
+import androidx.pdf.aidl.Dimensions;
+import androidx.pdf.aidl.LinkRects;
+import androidx.pdf.aidl.MatchRects;
+import androidx.pdf.aidl.PageSelection;
+import androidx.pdf.aidl.SelectionBoundary;
+import androidx.pdf.data.DisplayData;
+import androidx.pdf.data.FutureValue;
+import androidx.pdf.data.FutureValues.SettableFutureValue;
+import androidx.pdf.data.Openable;
+import androidx.pdf.data.PdfStatus;
+import androidx.pdf.data.Range;
+import androidx.pdf.fetcher.Fetcher;
+import androidx.pdf.util.ErrorLog;
+import androidx.pdf.util.ExternalLinks;
+import androidx.pdf.util.GestureTracker;
+import androidx.pdf.util.GestureTracker.GestureHandler;
+import androidx.pdf.util.ObservableValue.ValueObserver;
+import androidx.pdf.util.Preconditions;
+import androidx.pdf.util.ProjectorContext;
+import androidx.pdf.util.StrictModeUtils;
+import androidx.pdf.util.ThreadUtils;
+import androidx.pdf.util.TileBoard;
+import androidx.pdf.util.TileBoard.TileInfo;
+import androidx.pdf.util.Toaster;
+import androidx.pdf.util.Uris;
+import androidx.pdf.util.ZoomUtils;
+import androidx.pdf.viewer.PageViewFactory.PageView;
+import androidx.pdf.viewer.loader.PdfLoader;
+import androidx.pdf.viewer.loader.PdfLoaderCallbacks;
+import androidx.pdf.widget.FastScrollContentModel;
+import androidx.pdf.widget.FastScrollView;
+import androidx.pdf.widget.MosaicView.BitmapSource;
+import androidx.pdf.widget.ZoomView;
+import androidx.pdf.widget.ZoomView.ContentResizedMode;
+import androidx.pdf.widget.ZoomView.FitMode;
+import androidx.pdf.widget.ZoomView.InitialZoomMode;
+import androidx.pdf.widget.ZoomView.RotateMode;
+import androidx.pdf.widget.ZoomView.ZoomScroll;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A {@link Viewer} that can display paginated PDFs. Each page is rendered in its own View.
+ * Rendering is done in 2 passes:
+ *
+ * <ol>
+ *   <li>Layout: Request the dimensions of the page and set them as measure for the image view,
+ *   <li>Render: Create bitmap(s) at adequate dimensions and attach them to the page view.
+ * </ol>
+ *
+ * <p>The layout pass is progressive: starts with a few first pages of the document, then reach
+ * further as the user scrolls down (and ultimately spans the whole document). The rendering pass is
+ * tightly limited to the currently visible pages. Pages that are scrolled past (become not visible)
+ * have their bitmaps released to free up memory.
+ *
+ * <p>This is a {@link #SELF_MANAGED_CONTENTS} Viewer: its contents and internal models are kept
+ * when the view is destroyed, and re-used when the view is re-created.
+ *
+ * <p>Major lifecycle events include:
+ *
+ * <ol>
+ *   <li>{@link #onContentsAvailable} / {@link #onDestroy} : Content model is created.
+ *   <li>{@link #onCreateView} / {@link #destroyView} : All views are created, the pdf service is
+ *       connected.
+ * </ol>
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings({"UnusedMethod", "UnusedVariable"})
+public class PdfViewer extends LoadingViewer implements FastScrollContentModel {
+
+    private static final String TAG = "PdfViewer";
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    /** {@link View#setElevation(float)} value for PDF Pages (API 21+). */
+    private static final int PAGE_ELEVATION_DP = 2;
+
+    /** Key for saving {@link #mPageLayoutReach} in bundles. */
+    private static final String KEY_LAYOUT_REACH = "plr";
+
+    private static final String KEY_SPACE_LEFT = "leftSpace";
+    private static final String KEY_SPACE_TOP = "topSpace";
+    private static final String KEY_SPACE_BOTTOM = "bottomSpace";
+    private static final String KEY_SPACE_RIGHT = "rightSpace";
+    private static final String KEY_QUIT_ON_ERROR = "quitOnError";
+    private static final String KEY_EXIT_ON_CANCEL = "exitOnCancel";
+
+    /** Key to save/retrieve {@link #mEditingAuthorized} from Bundle. */
+    private static final String KEY_EDITING_AUTHORIZED = "editingAuthorized";
+
+    /** Single access to the PDF document: loads contents asynchronously (bitmaps, text,...) */
+    private PdfLoader mPdfLoader;
+
+    /** The file being displayed by this viewer. */
+    private DisplayData mFileData;
+
+    /** Callbacks of PDF loading asynchronous tasks. */
+    @VisibleForTesting
+    public final PdfLoaderCallbacks mPdfLoaderCallbacks;
+
+    /** Observer of the page position that controls loading of relevant PDF assets. */
+    private final ValueObserver<ZoomScroll> mZoomScrollObserver;
+
+    /** Observer to be set when the view is created. */
+    @Nullable
+    private ValueObserver<ZoomScroll> mPendingScrollPositionObserver;
+
+    private Object mScrollPositionObserverKey;
+
+    /** The number of pages of this PDF, set to -1 when not available. */
+    private int mNumPages = -1;
+
+    /** The range of currently visible pages. */
+    private Range mVisiblePages;
+
+    /** The highest number page reached. */
+    private int mMaxPage = -1;
+
+    private int mInitialPageLayoutReach = 4;
+
+    /** The number of pages that have been laid out in the document. */
+    private int mPageLayoutReach;
+
+    /** The last stable zoom: we only re-draw bitmaps at stable zoom (not during a gesture). */
+    private float mStableZoom;
+
+    private ZoomView mZoomView;
+
+    private PaginatedView mPaginatedView;
+    private PaginationModel mPaginationModel;
+
+    private PageIndicator mPageIndicator;
+
+    private SearchModel mSearchModel;
+    private PdfSelectionModel mSelectionModel;
+    private PdfSelectionHandles mSelectionHandles;
+    private final ValueObserver<String> mSearchQueryObserver;
+    private final ValueObserver<SelectedMatch> mSelectedMatchObserver;
+    private final ValueObserver<PageSelection> mSelectionObserver;
+    private final ValueObserver<Integer> mFastscrollerPositionObserver;
+    private Object mFastscrollerPositionObserverKey;
+    private FastScrollView mFastScrollView;
+
+    private boolean mDocumentLoaded = false;
+    /**
+     * After the document content is saved over the original in InkActivity, we set this bit to true
+     * so we know to callwhen the new document content is loaded.
+     */
+    private boolean mShouldRedrawOnDocumentLoaded = false;
+
+    @Nullable
+    private SettableFutureValue<Boolean> mPrintableVersionCallback;
+
+    // Non-null when a save-as operation is in progress. Cleared when operation is complete and
+    // value has been set with success/failure result.
+    @Nullable
+    private SettableFutureValue<Boolean> mSaveAsCallback;
+
+    // Base padding for ZoomView in px as set in saveZoomViewBasePadding().
+    private Rect mZoomViewBasePadding = new Rect();
+    private boolean mZoomViewBasePaddingSaved;
+
+    private boolean mWaitingOnSelectionToCreateInlineComment;
+    private boolean mEditingAuthorized;
+
+    /** Only interact with Queue on the main thread. */
+    private final List<OnDimensCallback> mDimensCallbackQueue = new ArrayList<>();
+
+    /** Callback is called everytime dimensions for a page have loaded. */
+    private interface OnDimensCallback {
+        /** Return true to continue receiving callbacks, else false. */
+        boolean onDimensLoaded(int pageNum);
+    }
+
+    public PdfViewer() {
+        super(SELF_MANAGED_CONTENTS);
+    }
+
+    @Override
+    public void configureShareScroll(boolean left, boolean right, boolean top, boolean bottom) {
+        mZoomView.setShareScroll(left, right, top, bottom);
+    }
+
+    /**
+     * If set, this Viewer will call {@link Activity#finish()} if it can't load the PDF. By default,
+     * the value is false.
+     */
+    @CanIgnoreReturnValue
+    public PdfViewer setQuitOnError(boolean quit) {
+        getArguments().putBoolean(KEY_QUIT_ON_ERROR, quit);
+        return this;
+    }
+
+    /**
+     * If set, this viewer will finish the attached activity when the user presses cancel on the
+     * prompt for the document password.
+     */
+    @CanIgnoreReturnValue
+    public PdfViewer setExitOnPasswordCancel(boolean shouldExitOnPasswordCancel) {
+        getArguments().putBoolean(KEY_EXIT_ON_CANCEL, shouldExitOnPasswordCancel);
+        return this;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mFetcher = Fetcher.build(getContext(), 1);
+    }
+
+    @SuppressLint("InflateParams")
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        super.onCreateView(inflater, container, savedState);
+        mPaginationModel = new PaginationModel();
+        mFastScrollView = (FastScrollView) inflater.inflate(R.layout.file_viewer_pdf, null);
+
+        mZoomView = mFastScrollView.findViewById(R.id.zoom_view);
+        mZoomView.setStraightenVerticalScroll(true);
+
+        mZoomView
+                .setFitMode(FitMode.FIT_TO_WIDTH)
+                .setInitialZoomMode(InitialZoomMode.ZOOM_TO_FIT)
+                .setRotateMode(RotateMode.KEEP_SAME_VIEWPORT_WIDTH)
+                .setContentResizedModeX(ContentResizedMode.KEEP_SAME_RELATIVE);
+
+        // Setting an id so that the View can restore itself. The Id has to be unique and
+        // predictable. An alternative that doesn't require id is to rely on this Fragment's
+        // onSaveInstanceState().
+        mZoomView.setId(getId() * 100);
+        mPaginatedView = mFastScrollView.findViewById(R.id.pdf_view);
+
+        mVisiblePages = new Range();
+        mPageLayoutReach = 0;
+
+        mPageIndicator = new PageIndicator(getActivity(), mFastScrollView);
+        applyReservedSpace();
+        mFastscrollerPositionObserver.onChange(null, mFastScrollView.getScrollerPositionY().get());
+        mFastscrollerPositionObserverKey =
+                mFastScrollView.getScrollerPositionY().addObserver(mFastscrollerPositionObserver);
+
+        // The view system requires the document loaded in order to be properly initialized, so
+        // we delay anything view-related until ViewState.VIEW_READY.
+        mZoomView.setVisibility(View.GONE);
+
+        mFastScrollView.setScrollable(this);
+        mFastScrollView.setId(getId() * 10);
+        return mFastScrollView;
+    }
+
+    private void applyReservedSpace() {
+        if (getArguments().containsKey(KEY_SPACE_TOP)) {
+            saveZoomViewBasePadding();
+            int left = getArguments().getInt(KEY_SPACE_LEFT, 0);
+            int top = getArguments().getInt(KEY_SPACE_TOP, 0);
+            int right = getArguments().getInt(KEY_SPACE_RIGHT, 0);
+            int bottom = getArguments().getInt(KEY_SPACE_BOTTOM, 0);
+
+            mPageIndicator.getView().setTranslationX(-right);
+
+            mZoomView.setPadding(
+                    mZoomViewBasePadding.left + left,
+                    mZoomViewBasePadding.top + top,
+                    mZoomViewBasePadding.right + right,
+                    mZoomViewBasePadding.bottom + bottom);
+
+            // Adjust the scroll bar to also include the same padding.
+            mFastScrollView.setScrollbarMarginTop(mZoomView.getPaddingTop());
+            mFastScrollView.setScrollbarMarginRight(right);
+            mFastScrollView.setScrollbarMarginBottom(mZoomView.getPaddingBottom());
+        }
+    }
+
+    /**
+     * Saves the padding set on {@link ZoomView} following initial inflation from XML.
+     *
+     * <p>This does not have to be called immediately following inflation but <i>must</i> be called
+     * before any methods change the padding on {@link ZoomView}.
+     *
+     * <p>This can be used by methods that need to set padding to (base padding + some other
+     * dimension). If these values were obtained directly from {@link ZoomView} or this method was
+     * allowed to execute multiple times it could result in padding expanding continually.
+     */
+    private void saveZoomViewBasePadding() {
+        if (mZoomView == null || mZoomViewBasePaddingSaved) {
+            return;
+        }
+
+        mZoomViewBasePadding =
+                new Rect(
+                        mZoomView.getPaddingLeft(),
+                        mZoomView.getPaddingTop(),
+                        mZoomView.getPaddingRight(),
+                        mZoomView.getPaddingBottom());
+
+        mZoomViewBasePadding.top +=
+                getResources().getDimensionPixelSize(R.dimen.viewer_doc_additional_top_offset);
+
+        mZoomViewBasePaddingSaved = true;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mZoomView.zoomScroll().addObserver(mZoomScrollObserver);
+        if (mPendingScrollPositionObserver != null) {
+            mScrollPositionObserverKey = mZoomView.zoomScroll().addObserver(
+                    mPendingScrollPositionObserver);
+            mPendingScrollPositionObserver = null;
+        }
+    }
+
+    @Override
+    protected void onContentsAvailable(DisplayData contents, @Nullable Bundle savedState) {
+        mFileData = contents;
+
+        // TODO: StrictMode- disk read 58ms.
+        int lengthMb = StrictModeUtils.bypassAndReturn(() -> (int) (contents.length() >> 20));
+        Log.v(TAG, "File length in MB: " + lengthMb);
+        createContentModel(
+                PdfLoader.create(
+                        getActivity().getApplicationContext(),
+                        contents,
+                        TileBoard.DEFAULT_RECYCLER,
+                        mPdfLoaderCallbacks,
+                        false));
+
+        if (savedState != null) {
+            int layoutReach = savedState.getInt(KEY_LAYOUT_REACH);
+            mEditingAuthorized = savedState.getBoolean(KEY_EDITING_AUTHORIZED);
+            mInitialPageLayoutReach = Math.max(mInitialPageLayoutReach, layoutReach);
+            Log.v(TAG, "Restore current reach " + mInitialPageLayoutReach);
+        }
+    }
+
+    @Override
+    protected void onEnter() {
+        super.onEnter();
+        // This is necessary for password protected PDF documents. If the user failed to produce the
+        // correct password, we want to prompt for the correct password every time the film strip
+        // comes back to this viewer.
+        if (!mDocumentLoaded && mPdfLoader != null) {
+            mPdfLoader.reconnect();
+        }
+
+        if (mPaginatedView != null && mPaginatedView.getChildCount() > 0) {
+            loadPageAssets(mZoomView.zoomScroll().get());
+        }
+    }
+
+    @Override
+    public void onExit() {
+        if (mVisiblePages != null && mVisiblePages.getLast() > mMaxPage) {
+            mMaxPage = mVisiblePages.getLast();
+        }
+
+        super.onExit();
+        if (!mDocumentLoaded && mPdfLoader != null) {
+            // e.g. a password-protected pdf that wasn't loaded.
+            mPdfLoader.disconnect();
+        }
+
+        if (mPaginatedView != null && mPaginatedView.getChildCount() > 0) {
+            for (PageMosaicView page : mPaginatedView.getChildViews()) {
+                page.clearTiles();
+                if (mPdfLoader != null) {
+                    mPdfLoader.cancelAllTileBitmaps(page.getPageNum());
+                }
+            }
+        }
+    }
+
+    private void createContentModel(PdfLoader pdfLoader) {
+        this.mPdfLoader = pdfLoader;
+
+        mSearchModel = new SearchModel(pdfLoader);
+        mSearchModel.query().addObserver(mSearchQueryObserver);
+        mSearchModel.selectedMatch().addObserver(mSelectedMatchObserver);
+
+        mSelectionModel = new PdfSelectionModel(pdfLoader);
+        mSelectionModel.selection().addObserver(mSelectionObserver);
+
+        mSelectionHandles = new PdfSelectionHandles(mSelectionModel, mZoomView, mPaginatedView);
+
+    }
+
+    private void destroyContentModel() {
+
+        mPageIndicator = null;
+
+        mSelectionHandles.destroy();
+        mSelectionHandles = null;
+
+        mSelectionModel.selection().removeObserver(mSelectionObserver);
+        mSelectionModel = null;
+
+        mSearchModel.selectedMatch().removeObserver(mSelectedMatchObserver);
+        mSearchModel.query().removeObserver(mSearchQueryObserver);
+        mSearchModel = null;
+
+        mPdfLoader.disconnect();
+        mPdfLoader = null;
+        mDocumentLoaded = false;
+    }
+
+    /**
+     *
+     */
+    public void setPassword(String password) {
+        if (mPdfLoader != null) {
+            mPdfLoader.applyPassword(password);
+        }
+    }
+
+    @Override
+    public void destroyView() {
+        if (mZoomView != null) {
+            mZoomView.zoomScroll().removeObserver(mZoomScrollObserver);
+            if (mScrollPositionObserverKey != null) {
+                mZoomView.zoomScroll().removeObserver(mScrollPositionObserverKey);
+            }
+            mZoomView = null;
+        }
+
+        if (mPaginatedView != null) {
+            mPaginatedView.removeAllViews();
+            mPaginationModel.removeObserver(mPaginatedView);
+            mPaginatedView = null;
+        }
+        // Clears the model so we can start fresh if we rebuild views.
+        mPaginationModel = new PaginationModel();
+        mVisiblePages = null;
+
+        if (mPdfLoader != null) {
+            mPdfLoader.cancelAll();
+            mPdfLoader.disconnect();
+            mDocumentLoaded = false;
+        }
+        mZoomViewBasePadding = new Rect();
+        mZoomViewBasePaddingSaved = false;
+        super.destroyView();
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        if (mFastscrollerPositionObserverKey != null && mFastScrollView != null) {
+            mFastScrollView.getScrollerPositionY().removeObserver(mFastscrollerPositionObserverKey);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mPdfLoader != null) {
+            destroyContentModel();
+        }
+        mPrintableVersionCallback = null;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putInt(KEY_LAYOUT_REACH, mPageLayoutReach);
+        Log.v(TAG, "Saved current reach " + mPageLayoutReach);
+
+        outState.putBoolean(KEY_EDITING_AUTHORIZED, mEditingAuthorized);
+    }
+
+    @Override
+    public long getContentLength() {
+        return getPageCount();
+    }
+
+    @Override
+    public int getViewProgress() {
+        if (mMaxPage > 0) {
+            return (int) (((double) mMaxPage / getPageCount() * 100) * PROGRESS_SCALER);
+        }
+        return -1;
+    }
+
+    /**
+     * Load the PDF document.
+     *
+     * @param fileUri URI of the document.
+     */
+    public void loadFile(@NonNull Uri fileUri) {
+        Preconditions.checkNotNull(fileUri);
+        try {
+            validateFileUri(fileUri);
+        } catch (SecurityException e) {
+            // TODO Toaster.LONG.popToast(this, R.string.problem_with_file);
+            Log.e(TAG, e.getMessage());
+            finishActivity();
+        }
+
+        fetchFile(fileUri);
+    }
+
+    private void validateFileUri(Uri fileUri) {
+        if (!Uris.isContentUri(fileUri) && !Uris.isFileUri(fileUri)) {
+            throw new IllegalArgumentException("Only content and file uri is supported");
+        }
+
+        // TODO confirm this exception
+        if (Uris.isFileUriInSamePackageDataDir(fileUri)) {
+            throw new SecurityException(
+                    "Disallow opening file:// URIs in the parent package's data directory for "
+                            + "security reasons");
+        }
+    }
+
+    private void fetchFile(@NonNull final Uri fileUri) {
+        Preconditions.checkNotNull(fileUri);
+        final String fileName = getFileName(fileUri);
+        final FutureValue<Openable> openable;
+        openable = mFetcher.loadLocal(fileUri);
+
+        // Only make this visible when we know a file needs to be fetched.
+        // TODO loadingScreen.setVisibility(View.VISIBLE);
+
+        openable.get(
+                new FutureValue.Callback<Openable>() {
+                    @Override
+                    public void available(Openable openable) {
+                        viewerAvailable(fileUri, fileName, openable);
+                    }
+
+                    @Override
+                    public void failed(Throwable thrown) {
+                        ErrorLog.log(TAG, "fetchFile:" + fileUri.getScheme(), thrown);
+                        finishActivity();
+                    }
+
+                    @Override
+                    public void progress(float progress) {
+                    }
+                });
+    }
+
+    private void finishActivity() {
+        if (getActivity() != null) {
+            getActivity().finish();
+        }
+    }
+
+    @Nullable
+    private ContentResolver getResolver() {
+        if (getActivity() != null) {
+            return getActivity().getContentResolver();
+        }
+        return null;
+    }
+
+    private String getFileName(@NonNull Uri fileUri) {
+        ContentResolver resolver = getResolver();
+        return resolver != null ? Uris.extractName(fileUri, resolver) : Uris.extractFileName(
+                fileUri);
+    }
+
+    private void viewerAvailable(Uri fileUri, String fileName, Openable openable) {
+        DisplayData contents = new DisplayData(fileUri, fileName, openable);
+
+        // TODO loadingScreen.setVisibility(View.GONE);
+
+        startViewer(contents);
+    }
+
+    private void startViewer(@NonNull DisplayData contents) {
+        Preconditions.checkNotNull(contents);
+
+        setQuitOnError(true);
+        setExitOnPasswordCancel(true);
+        feed(contents);
+        postEnter();
+    }
+
+    private int getPageCount() {
+        return mNumPages;
+    }
+
+    /** Returns the page currently roughly centered in the view. */
+    public int getViewingPage() {
+        return (mVisiblePages != null) ? (mVisiblePages.getFirst() + mVisiblePages.getLast()) / 2
+                : 0;
+    }
+
+    /**
+     * Lay out some pages up to some distant page. Not guaranteed to lay out any pages: maybe all
+     * pages, or at least enough pages, are already laid out.
+     */
+    private void maybeLayoutPages(int current) {
+        int peekAhead = Math.min(current + 2, 100);
+        int distantPage = Math.max(current + peekAhead, mInitialPageLayoutReach);
+        layoutPages(distantPage);
+    }
+
+    /**
+     * Lays out all the pages until {@code untilPage}, or equivalently so that {@code untilPage}s
+     * are laid out. So calling with {@code untilPage = 10} will ensure pages 0-9 are laid out.
+     *
+     * @param untilPage The upper limit of the range of pages to be laid out. Cropped to the
+     *                  number of pages of the document if this number was larger.
+     */
+    private void layoutPages(int untilPage) {
+        if (mPdfLoader == null) {
+            ErrorLog.log(TAG, "ERROR Can't layout pages as no pdfLoader " + mPageLayoutReach);
+            return;
+        }
+        boolean pushed = false;
+        int lastPage = Math.min(untilPage, getPageCount());
+        int requestLayoutPage = mPageLayoutReach;
+        while (requestLayoutPage < lastPage) {
+            mPdfLoader.loadPageDimensions(requestLayoutPage);
+            requestLayoutPage++;
+            pushed = true;
+        }
+
+        if (pushed) {
+            Log.v(TAG, "Pushed the boundaries of known pages to " + requestLayoutPage);
+        }
+    }
+
+    private PageView createPage(final int pageNum) {
+        BitmapSource bitmapSource =
+                new BitmapSource() {
+
+                    @Override
+                    public void requestPageBitmap(Dimensions pageSize,
+                            boolean alsoRequestingTiles) {
+                        mPdfLoader.loadPageBitmap(pageNum, pageSize);
+                    }
+
+                    @Override
+                    public void requestNewTiles(Dimensions pageSize, Iterable<TileInfo> tiles) {
+                        mPdfLoader.loadTileBitmaps(pageNum, pageSize, tiles);
+                    }
+
+                    @Override
+                    public void cancelTiles(Iterable<Integer> tileIds) {
+                        mPdfLoader.cancelTileBitmaps(pageNum, tileIds);
+                    }
+                };
+        Dimensions dimensions = mPaginationModel.getPageSize(pageNum);
+        PageView pageView =
+                PageViewFactory.createPageView(
+                        getActivity(),
+                        pageNum,
+                        dimensions,
+                        bitmapSource,
+                        TileBoard.DEFAULT_RECYCLER,
+                        mZoomView.zoomScroll());
+        mPaginatedView.addView(pageView);
+
+        GestureTracker gestureTracker = new GestureTracker("PageView", getActivity());
+        pageView.asView().setOnTouchListener(gestureTracker);
+        gestureTracker.setDelegateHandler(new PageTouchListener(pageView));
+
+        PageMosaicView pageMosaicView = pageView.getPageView();
+        // Setting Elevation only works if there is a background color.
+        pageMosaicView.setBackgroundColor(Color.WHITE);
+        pageMosaicView.setElevation(ProjectorContext.get().getScreen().pxFromDp(PAGE_ELEVATION_DP));
+        return pageView;
+    }
+
+    /**
+     * Convenience method that always returns a child view. However in the case it's just been
+     * created, that View is not laid out.
+     */
+    private PageView getOrCreatePage(int pageNum) {
+        PageView pageView = mPaginatedView.getViewAt(pageNum);
+        if (pageView != null) {
+            return pageView;
+        }
+        return createPage(pageNum);
+    }
+
+    private boolean isPageCreated(int pageNum) {
+        return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+    }
+
+    private PageView getPage(int pageNum) {
+        return mPaginatedView.getViewAt(pageNum);
+    }
+
+    private Range allPages() {
+        return new Range(0, mPaginationModel.getSize() - 1);
+    }
+
+    private void lookAtSelection(SelectedMatch selection) {
+        if (selection == null || selection.isEmpty()) {
+            return;
+        }
+        if (selection.getPage() >= mPaginationModel.getSize()) {
+            layoutPages(selection.getPage() + 1);
+            return;
+        }
+        Rect rect = selection.getPageMatches().getFirstRect(selection.getSelected());
+        int x = mPaginationModel.getLookAtX(selection.getPage(), rect.centerX());
+        int y = mPaginationModel.getLookAtY(selection.getPage(), rect.centerY());
+        mZoomView.centerAt(x, y);
+
+        PageMosaicView pageView = getOrCreatePage(selection.getPage()).getPageView();
+        pageView.setOverlay(selection.getOverlay());
+    }
+
+    private void loadPageAssets(ZoomScroll position) {
+
+        // 1. Refresh visible pages and view area.
+        mVisiblePages = computeVisibleRange(position);
+
+        if (mVisiblePages.getLast() > mMaxPage) {
+            mMaxPage = mVisiblePages.getLast();
+        }
+
+        // Change the resolution of the bitmaps only when a gesture is not in progress.
+        if (position.stable || mStableZoom == 0) {
+            mStableZoom = position.zoom;
+        }
+
+        mPaginationModel.setViewArea(mZoomView.getVisibleAreaInContentCoords());
+
+        Range allPages = allPages();
+        int prefetchRadius = 1; // Note: could make this variable depending on resolution.
+        Range nearPages = expandRange(mVisiblePages, prefetchRadius, allPages);
+
+        // 2. Release pages that we don't need anymore.
+        Range[] gonePages = allPages.minus(nearPages);
+        for (Range pages : gonePages) {
+            // Keep Views around for now, we'll clear them in step (4) if applicable.
+            clearPages(pages, false);
+        }
+
+        // 3. Bring minimal service to pages that we might need very soon
+        Range[] invisibleNearPages = nearPages.minus(mVisiblePages);
+        for (Range pages : invisibleNearPages) {
+            loadPageOnly(pages);
+        }
+
+        // The step (4) below requires page Views to be created and laid out. So we create them here
+        // and set this flag if that operation needs to wait for a layout pass.
+        boolean requiresLayoutPass = false;
+        for (int pageNum : mVisiblePages) {
+            if (mPaginatedView.getViewAt(pageNum) == null) {
+                createPage(pageNum);
+                requiresLayoutPass = true;
+            }
+        }
+
+        // 4. Refresh tiles and/or full pages.
+        if (position.stable) {
+            // Perform a full refresh on all visible pages
+            if (requiresLayoutPass) {
+                refreshPagesAfterLayout(mVisiblePages);
+            } else {
+                refreshPages(mVisiblePages);
+            }
+            for (Range pages : gonePages) {
+                clearPages(pages, true);
+            }
+        } else if (mStableZoom == position.zoom) {
+            // Just load a few more tiles in case of tile-scroll
+            if (requiresLayoutPass) {
+                refreshTilesAfterLayout(mVisiblePages);
+            } else {
+                refreshTiles(mVisiblePages);
+            }
+        }
+
+        maybeLayoutPages(mVisiblePages.getLast());
+    }
+
+    private void refreshTiles(Range pages) {
+        for (int page : pages) {
+            PageMosaicView pageView = getOrCreatePage(page).getPageView();
+            pageView.requestTiles();
+        }
+    }
+
+    private void refreshTilesAfterLayout(final Range pages) {
+        ThreadUtils.postOnUiThread(
+                () -> {
+                    if (viewState().get() != ViewState.NO_VIEW) {
+                        refreshTiles(pages);
+                    }
+                });
+    }
+
+    private void loadPageOnly(Range pages) {
+        for (int page : pages) {
+            mPdfLoader.cancelAllTileBitmaps(page);
+            PageMosaicView pageView = getOrCreatePage(page).getPageView();
+            pageView.clearTiles();
+            pageView.requestFastDrawAtZoom(mStableZoom);
+            loadVisiblePageText(page);
+            maybeLoadFormAccessibilityInfo(page);
+        }
+    }
+
+    private void refreshPages(Range pages) {
+        for (int page : pages) {
+            PageMosaicView pageView = getOrCreatePage(page).getPageView();
+            pageView.requestDrawAtZoom(mStableZoom);
+            loadVisiblePageText(page);
+            maybeLoadFormAccessibilityInfo(page);
+        }
+    }
+
+    private void refreshPagesAfterLayout(final Range pages) {
+        ThreadUtils.postOnUiThread(
+                () -> {
+                    if (viewState().get() != ViewState.NO_VIEW) {
+                        refreshPages(pages);
+                    }
+                });
+    }
+
+    private void clearPages(Range pages, boolean clearViews) {
+        for (int page : pages) {
+            // Don't cancel search - search results for the current search are always useful,
+            // even for pages we can't see right now. Form filling operations should always
+            // be executed against the document, even if the user has scrolled away from the page.
+            mPdfLoader.cancelExceptSearchAndFormFilling(page);
+            if (clearViews) {
+                mPaginatedView.removeViewAt(page);
+            }
+        }
+    }
+
+    private void loadVisiblePageText(int page) {
+        PageView pageView = getOrCreatePage(page);
+        PageMosaicView pageMosaicView = pageView.getPageView();
+        if (pageMosaicView.needsPageText()) {
+            mPdfLoader.loadPageText(page);
+        }
+        if (!pageMosaicView.hasPageUrlLinks()) {
+            mPdfLoader.loadPageUrlLinks(page);
+        }
+
+        if (page == mSelectionModel.getPage()) {
+            pageMosaicView.setOverlay(new PdfHighlightOverlay(mSelectionModel.selection().get()));
+        } else if (mSearchModel.query().get() != null) {
+            if (!pageMosaicView.hasOverlay()) {
+                mPdfLoader.searchPageText(page, mSearchModel.query().get());
+            }
+        } else {
+            pageMosaicView.setOverlay(null);
+        }
+    }
+
+    /**
+     * Load accessibility information for the form if document can be edited and accessibility is
+     * required.
+     */
+    private void maybeLoadFormAccessibilityInfo(int pageNum) {
+        getOrCreatePage(pageNum);
+    }
+
+    /** Computes the range of visible pages in the given position. */
+    private Range computeVisibleRange(ZoomScroll position) {
+        int top = Math.round(position.scrollY / position.zoom);
+        int bottom = Math.round((position.scrollY + mZoomView.getHeight()) / position.zoom);
+        Range window = new Range(top, bottom);
+        return mPaginationModel.getPagesInWindow(window, true);
+    }
+
+    /** Expand the range to include more page(s) in the each direction. */
+    private static Range expandRange(Range range, int margin, Range allPages) {
+        return range.expand(margin, allPages);
+    }
+
+    /**
+     * Computes the range of pages that are entirely visible, or if no page is entirely visible,
+     * returns the most visible page.
+     */
+    private Range computeImportantRange(ZoomScroll position) {
+        int top = Math.round(position.scrollY / position.zoom);
+        int bottom = Math.round((position.scrollY + mZoomView.getHeight()) / position.zoom);
+        Range window = new Range(top, bottom);
+        return mPaginationModel.getPagesInWindow(window, false);
+    }
+
+    { // Listen to ZoomView.
+        mZoomScrollObserver =
+                new ValueObserver<ZoomScroll>() {
+                    @Override
+                    public void onChange(ZoomScroll oldPosition, ZoomScroll position) {
+                        loadPageAssets(position);
+                        if (mPageIndicator.setRangeAndZoom(
+                                computeImportantRange(position), position.zoom, position.stable)) {
+                            showFastScrollView();
+                        }
+                    }
+
+                    @Override
+                    public String toString() {
+                        return TAG + "#zoomScrollObserver";
+                    }
+                };
+    }
+
+    { // Listen to searchModel.
+        mSearchQueryObserver =
+                new ValueObserver<String>() {
+                    @Override
+                    public void onChange(String oldQuery, String newQuery) {
+                        mPaginatedView.clearAllOverlays();
+                    }
+
+                    @Override
+                    public String toString() {
+                        return TAG + "#searchQueryObserver";
+                    }
+                };
+
+        mSelectedMatchObserver =
+                new ValueObserver<SelectedMatch>() {
+                    @Override
+                    public void onChange(
+                            @Nullable SelectedMatch oldSelection,
+                            @Nullable SelectedMatch newSelection) {
+                        if (newSelection == null) {
+                            mPaginatedView.clearAllOverlays();
+                            return;
+                        }
+                        if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
+                            // Selected match has moved onto a new page - update the overlay on
+                            // the old page.
+                            getPage(oldSelection.getPage())
+                                    .getPageView()
+                                    .setOverlay(
+                                            new PdfHighlightOverlay(oldSelection.getPageMatches()));
+                        }
+                        lookAtSelection(newSelection);
+                    }
+
+                    @Override
+                    public String toString() {
+                        return TAG + "#selectedMatchObserver";
+                    }
+                };
+    }
+
+    { // Listen to selectionModel.
+        mSelectionObserver =
+                new ValueObserver<PageSelection>() {
+                    @Override
+                    public void onChange(PageSelection oldSelection, PageSelection newSelection) {
+                        if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
+                            getPage(oldSelection.getPage()).getPageView().setOverlay(null);
+                        }
+                        if (newSelection != null && mVisiblePages.contains(
+                                newSelection.getPage())) {
+                            getOrCreatePage(newSelection.getPage())
+                                    .getPageView()
+                                    .setOverlay(new PdfHighlightOverlay(newSelection));
+                        }
+                    }
+
+                    @NonNull
+                    @Override
+                    public String toString() {
+                        return TAG + "#selectionObserver";
+                    }
+                };
+    }
+
+    {
+        mFastscrollerPositionObserver =
+                new ValueObserver<Integer>() {
+                    @Override
+                    public void onChange(@Nullable Integer oldValue, @Nullable Integer newValue) {
+                        if (mPageIndicator != null && newValue != null) {
+                            mPageIndicator.getView().setY(
+                                    newValue - (mPageIndicator.getView().getHeight() / 2));
+                            mPageIndicator.show();
+                            showFastScrollView();
+                        }
+                    }
+
+                    @Override
+                    public String toString() {
+                        return TAG + "#fastscrollerPositionObserver";
+                    }
+                };
+    }
+
+    /** Gesture listener for PageView's handling of tap and long press. */
+    private class PageTouchListener extends GestureHandler {
+
+        private final PageView mPageView;
+
+        PageTouchListener(PageView pageView) {
+            this.mPageView = pageView;
+        }
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            return true;
+        }
+
+        @Override
+        public boolean onSingleTapConfirmed(MotionEvent e) {
+            return handleSingleTapNoFormFilling(e);
+        }
+
+        @Override
+        public void onLongPress(MotionEvent e) {
+            SelectionBoundary boundary =
+                    SelectionBoundary.atPoint(new Point((int) e.getX(), (int) e.getY()));
+            mPdfLoader.selectPageText(mPageView.getPageNum(), boundary, boundary);
+        }
+
+        // TODO: Add method to track enter form-filling via widget.
+
+        /**
+         * Handles a tap event for non-formfilling actions.
+         *
+         * <p>This is includes comments, links and full screen toggle. Separate from form filling as
+         * form filling involves asynchronous evaluations that must be completed outside normal
+         * branch
+         * statements.
+         */
+        private boolean handleSingleTapNoFormFilling(MotionEvent e) {
+            boolean hadSelection =
+                    mSelectionModel != null && mSelectionModel.selection().get() != null;
+            if (hadSelection) {
+                mSelectionModel.setSelection(null);
+            }
+
+            Point point = new Point((int) e.getX(), (int) e.getY());
+            String linkUrl = mPageView.getPageView().getLinkUrl(point);
+            if (linkUrl != null) {
+                ExternalLinks.open(linkUrl, getActivity());
+            }
+
+            return true;
+        }
+    }
+
+    // TODO: Revisit this method for its usage. Currently redundant
+
+    /** Goes to the {@code pageNum} and fits the page to the current viewport. */
+    private void gotoPage(int pageNum) {
+        if (pageNum >= mPaginationModel.getSize()) {
+            // We have not yet loaded our destination.
+            layoutPages(pageNum + 1);
+            mDimensCallbackQueue.add(
+                    loadedPageNum -> {
+                        if (pageNum == loadedPageNum) {
+                            gotoPage(pageNum);
+                            return false;
+                        }
+                        return true;
+                    });
+            return;
+        }
+
+        Rect pageRect = mPaginationModel.getPageLocation(pageNum);
+
+        int x = pageRect.left + (pageRect.width() / 2);
+        int y = pageRect.top + (pageRect.height() / 2);
+        float zoom =
+                ZoomUtils.calculateZoomToFit(
+                        mZoomView.getViewportWidth(),
+                        mZoomView.getViewportHeight(),
+                        pageRect.width(),
+                        pageRect.height());
+
+        mZoomView.setZoom(zoom);
+        mZoomView.centerAt(x, y);
+    }
+
+    { // Init pdfLoaderCallbacks
+        mPdfLoaderCallbacks =
+                new PdfLoaderCallbacks() {
+                    // Callbacks should exit early if viewState == NO_VIEW (typically a Destroy
+                    // is in progress).
+                    @Override
+                    public void requestPassword(boolean incorrect) {
+                        throw new UnsupportedOperationException("TODO: PDF Password");
+                    }
+
+                    @Override
+                    public void documentLoaded(int numPages) {
+                        if (numPages <= 0) {
+                            documentNotLoaded(PdfStatus.PDF_ERROR);
+                            return;
+                        }
+
+                        mDocumentLoaded = true;
+                        PdfViewer.this.mNumPages = numPages;
+                        // Assume we see at least the first page
+                        mMaxPage = 1;
+                        if (viewState().get() != ViewState.NO_VIEW) {
+                            mPaginationModel.initialize(numPages);
+                            mPaginatedView.setModel(mPaginationModel);
+                            mPaginationModel.addObserver(mPaginatedView);
+
+                            maybeLayoutPages(1);
+                            mPageIndicator.setNumPages(numPages);
+                            mSearchModel.setNumPages(numPages);
+                        }
+
+                        if (mShouldRedrawOnDocumentLoaded) {
+                            mShouldRedrawOnDocumentLoaded = false;
+                        }
+                    }
+
+                    @Override
+                    public void documentNotLoaded(PdfStatus status) {
+                        if (viewState().get() != ViewState.NO_VIEW) {
+                            if (getArguments().getBoolean(KEY_QUIT_ON_ERROR)) {
+                                getActivity().finish();
+                            }
+                            switch (status) {
+                                case NONE:
+                                case FILE_ERROR:
+                                    handleError();
+                                    break;
+                                case PDF_ERROR:
+                                    Toaster.LONG.popToast(
+                                            getActivity(), R.string.error_file_format_pdf,
+                                            mFileData.getName());
+                                    break;
+                                case LOADED:
+                                case REQUIRES_PASSWORD:
+                                    Preconditions.checkArgument(
+                                            false,
+                                            "Document not loaded but status " + status.getNumber());
+                                    break;
+                                case PAGE_BROKEN:
+                                case NEED_MORE_DATA:
+                                    // no op.
+                            }
+                            // TODO: Tracker render error.
+                        }
+                    }
+
+                    @Override
+                    public void pageBroken(int page) {
+                        if (viewState().get() != ViewState.NO_VIEW) {
+                            getOrCreatePage(page)
+                                    .getPageView()
+                                    .setFailure(getString(R.string.error_on_page, page + 1));
+                            Toaster.LONG.popToast(getActivity(), R.string.error_on_page, page + 1);
+                            // TODO: Track render error.
+                        }
+                    }
+
+                    @Override
+                    public void setPageDimensions(int pageNum, Dimensions dimensions) {
+                        if (viewState().get() != ViewState.NO_VIEW) {
+                            mPaginationModel.addPage(pageNum, dimensions);
+                            mPageLayoutReach = mPaginationModel.getSize();
+
+                            if (mSearchModel.query().get() != null
+                                    && mSearchModel.selectedMatch().get() != null
+                                    && mSearchModel.selectedMatch().get().getPage() == pageNum) {
+                                // lookAtSelection is posted to run once layout has finished:
+                                ThreadUtils.postOnUiThread(
+                                        () -> {
+                                            if (viewState().get() != ViewState.NO_VIEW) {
+                                                lookAtSelection(mSearchModel.selectedMatch().get());
+                                            }
+                                        });
+                            }
+
+                            ThreadUtils.postOnUiThread(
+                                    () -> {
+                                        if (mDimensCallbackQueue.isEmpty()
+                                                || viewState().get() == ViewState.NO_VIEW) {
+                                            return;
+                                        }
+
+                                        Iterator<OnDimensCallback> iterator =
+                                                mDimensCallbackQueue.iterator();
+                                        while (iterator.hasNext()) {
+                                            OnDimensCallback callback = iterator.next();
+                                            boolean shouldKeep = callback.onDimensLoaded(pageNum);
+                                            if (!shouldKeep) {
+                                                iterator.remove();
+                                            }
+                                        }
+                                    });
+
+                            // The new page might actually be visible on the screen, so we need
+                            // to fetch assets:
+                            Range newRange = computeVisibleRange(mZoomView.zoomScroll().get());
+                            if (newRange.isEmpty()) {
+                                // During fast-scroll, we mostly don't need to fetch assets, but
+                                // make sure we keep pushing layout bounds far enough, and update
+                                // page numbers as we "scroll" down.
+                                if (mPageIndicator.setRangeAndZoom(newRange, mStableZoom, false)) {
+                                    showFastScrollView();
+                                }
+                                maybeLayoutPages(newRange.getLast());
+                            } else if (newRange.contains(pageNum)) {
+                                // The new page is visible, fetch its assets.
+                                loadPageAssets(mZoomView.zoomScroll().get());
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void setTileBitmap(int pageNum, TileInfo tileInfo, Bitmap bitmap) {
+                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
+                            getPage(pageNum).getPageView().setTileBitmap(tileInfo, bitmap);
+                        }
+                    }
+
+                    @Override
+                    public void setPageBitmap(int pageNum, Bitmap bitmap) {
+                        // We announce that the viewer is ready as soon as a bitmap is loaded
+                        // (not before).
+                        if (mViewState.get() == ViewState.VIEW_CREATED) {
+                            mZoomView.setVisibility(View.VISIBLE);
+                            mViewState.set(ViewState.VIEW_READY);
+                        }
+                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
+                            getPage(pageNum).getPageView().setPageBitmap(bitmap);
+                        }
+                    }
+
+                    @Override
+                    public void setPageText(int pageNum, String text) {
+                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
+                            getPage(pageNum).getPageView().setPageText(text);
+                        }
+                    }
+
+                    @Override
+                    public void setSearchResults(String query, int pageNum, MatchRects matches) {
+                        if (viewState().get() != ViewState.NO_VIEW && query.equals(
+                                mSearchModel.query().get())) {
+                            mSearchModel.updateMatches(query, pageNum, matches);
+                            if (isPageCreated(pageNum)) {
+                                getPage(pageNum)
+                                        .getPageView()
+                                        .setOverlay(
+                                                mSearchModel.getOverlay(query, pageNum, matches));
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void setSelection(int pageNum, @Nullable PageSelection selection) {
+                        if (viewState().get() == ViewState.NO_VIEW) {
+                            return;
+                        }
+                        if (selection != null) {
+                            if (mWaitingOnSelectionToCreateInlineComment) {
+                                mSelectionModel.setSelection(selection);
+                                return;
+                            }
+                            // Clear searchModel - we hide the search and show the selection
+                            // instead.
+                            mSearchModel.setQuery(null, -1);
+                        }
+                        mSelectionModel.setSelection(selection);
+                    }
+
+                    @Override
+                    public void setPageUrlLinks(int pageNum, LinkRects links) {
+                        if (viewState().get() != ViewState.NO_VIEW && links != null
+                                && isPageCreated(pageNum)) {
+                            getPage(pageNum).setPageUrlLinks(links);
+                        }
+                    }
+
+                    @Override
+                    public void documentCloned(boolean result) {
+                        if (mPrintableVersionCallback != null) {
+                            mPrintableVersionCallback.set(result);
+                        }
+                        mPrintableVersionCallback = null;
+                    }
+
+                    @Override
+                    public void documentSavedAs(boolean result) {
+                        if (mSaveAsCallback != null) {
+                            mSaveAsCallback.set(result);
+                        }
+                        mSaveAsCallback = null;
+                    }
+
+                    /**
+                     * Receives areas of a page that have been invalidated by an editing action
+                     * and asks the
+                     * appropriate page view to redraw them.
+                     */
+                    @Override
+                    public void setInvalidRects(int pageNum, List<Rect> invalidRects) {
+                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
+                            if (invalidRects == null || invalidRects.isEmpty()) {
+                                return;
+                            }
+                            mPaginatedView.getViewAt(pageNum).getPageView().requestRedrawAreas(
+                                    invalidRects);
+                        }
+                    }
+                };
+    }
+
+    @Override
+    public float estimateFullContentHeight() {
+        return mPaginationModel.getEstimatedFullHeight();
+    }
+
+    @Override
+    public float visibleHeight() {
+        return mZoomView.getViewportHeight() / mZoomView.getZoom();
+    }
+
+    @Override
+    public void fastScrollTo(float position, boolean stable) {
+        mZoomView.scrollTo(mZoomView.getScrollX(), (int) (position * mZoomView.getZoom()), stable);
+    }
+
+    @Override
+    public void setFastScrollListener(final FastScrollListener listener) {
+        mZoomView
+                .getViewTreeObserver()
+                .addOnScrollChangedListener(
+                        new OnScrollChangedListener() {
+                            @Override
+                            public void onScrollChanged() {
+                                if (mZoomView != null) {
+                                    listener.updateFastScrollbar(
+                                            mZoomView.getScrollY() / mZoomView.getZoom());
+                                }
+                            }
+                        });
+    }
+
+    protected void handleError() {
+        mViewState.set(ViewState.ERROR);
+    }
+
+
+    /** Create callback to retry password input when user cancels password prompt. */
+    public void setPasswordCancelError() {
+
+    }
+
+    private void showFastScrollView() {
+        if (mFastScrollView != null) {
+            mFastScrollView.setVisible();
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java
new file mode 100644
index 0000000..4ac0d36
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer;
+
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.aidl.MatchRects;
+import androidx.pdf.find.MatchCount;
+import androidx.pdf.util.CycleRange;
+import androidx.pdf.util.CycleRange.Direction;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.Observables;
+import androidx.pdf.util.Observables.ExposedValue;
+import androidx.pdf.util.Preconditions;
+import androidx.pdf.viewer.loader.PdfLoader;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Stores data relevant to the current search, including the query and the selected match, and the
+ * number of matches on each page.
+ *
+ * <p>SearchModel is responsible for starting SearchPageTextTasks for every page that it needs data
+ * for. It uses the pdfLoader to do this. Whenever the user updates a query or selects "find next"
+ * or "find previous", this class will update data about the number of matches on each page, and
+ * about which match is selected. The viewer can listen to changes in the selected data.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SearchModel {
+
+    /**
+     * The current query. Null if the user is not performing a search, or searches for
+     * whitespace.
+     */
+    private final ExposedValue<String> mQuery = Observables.newExposedValueWithInitialValue(null);
+
+    /**
+     * The currently selected match. Null if there the search query is null or there are no matches
+     * for the current query, or a match for the current query has not yet been found.
+     */
+    private final ExposedValue<SelectedMatch> mSelectedMatch =
+            Observables.newExposedValueWithInitialValue(null);
+
+    /**
+     * We store the last selected match so that when the next search finishes, we can select the
+     * match
+     * that is as close as possible to the last selected match.
+     */
+    private SelectedMatch mLastSelectedMatch = null;
+
+    /** The index of the current match out of the total matches found ie, match 3 of 8. */
+    private final ExposedValue<MatchCount> mMatchCount =
+            Observables.newExposedValueWithInitialValue(null);
+
+    private final PdfLoader mPdfLoader;
+
+    /**
+     * The number of matches of the current query found on each page. Remains {@code null} until the
+     * document is loaded and so the array length is known.
+     */
+    private int[] mPageToMatchCount;
+    /** The total number of matches of the current query, found on all pages so far. */
+    private int mTotalMatches = 0;
+
+    /**
+     * An iterator that spreads OUTWARDS from the current location. If null, counting all the
+     * matches
+     * hasn't started. If !hasNext(), counting the matches has finished - otherwise it is in
+     * progress.
+     */
+    private CycleRange.Iterator mNextPageToCount;
+
+    /**
+     * An iterator that spreads FORWARDS or BACKWARDS from the last selected match. If null, it
+     * means
+     * no find-next or find-previous operation in progress. If !hasNext(), it means the entire
+     * document was searched and no match was found. Otherwise, a find-next or find-previous is in
+     * progress, and if a match is found, then the selected match will be updated.
+     */
+    private CycleRange.Iterator mNextSelectedPage;
+
+    public SearchModel(PdfLoader pdfLoader) {
+        this.mPdfLoader = pdfLoader;
+    }
+
+    /** Set the number of pages the document has. */
+    public void setNumPages(int numPages) {
+        mPageToMatchCount = new int[numPages];
+        clearPageToMatchCount();
+    }
+
+    /** Return the number of pages the document has, or -1 if not yet known. */
+    public int getNumPages() {
+        return (mPageToMatchCount != null) ? mPageToMatchCount.length : -1;
+    }
+
+    /** Return the current search query. */
+    public ObservableValue<String> query() {
+        return mQuery;
+    }
+
+    /** Return the currently selected match. */
+    public ObservableValue<SelectedMatch> selectedMatch() {
+        return mSelectedMatch;
+    }
+
+    /** Return index of the current match out of the total matches found. */
+    public ObservableValue<MatchCount> matchCount() {
+        return mMatchCount;
+    }
+
+    /**
+     * Returns the page that the currently selected match is on, or -1 if there is no currently
+     * selected match.
+     */
+    public int getSelectedPage() {
+        SelectedMatch value = mSelectedMatch.get();
+        return (value != null) ? value.getPage() : -1;
+    }
+
+    /** Set query for new search. */
+    public void setQuery(@Nullable String newQuery, int viewingPage) {
+        newQuery = whiteSpaceToNull(newQuery);
+        if (!Objects.equals(mQuery.get(), newQuery)) {
+            mQuery.set(newQuery);
+            mSelectedMatch.set(null);
+            clearPageToMatchCount();
+            if (newQuery != null) {
+                startNewSearch(newQuery, viewingPage);
+            } else {
+                mLastSelectedMatch = null;
+            }
+        }
+    }
+
+    private void startNewSearch(String newQuery, int viewingPage) {
+        if (getNumPages() < 0) {
+            return; // Cannot search until setNumPages is called.
+        }
+
+        // Start on the page the last selected match was on, if there was one.
+        // If not then start on the page the user is viewing.
+        int startPage = (mLastSelectedMatch != null) ? mLastSelectedMatch.getPage() : viewingPage;
+
+        // Make a plan to select a match, starting here and going forwards until we find the match.
+        mNextSelectedPage = CycleRange.of(startPage, getNumPages(), Direction.FORWARDS).iterator();
+        // Make a plan to count all matches on every page, starting here and going outwards until
+        // there are no more pages to count.
+        mNextPageToCount = CycleRange.of(startPage, getNumPages(), Direction.OUTWARDS).iterator();
+        mPdfLoader.searchPageText(startPage, newQuery);
+    }
+
+    /** Clears pageToMatchCount array, nextPageToCount and totalMatches. */
+    private void clearPageToMatchCount() {
+        if (mPageToMatchCount != null) {
+            Arrays.fill(mPageToMatchCount, -1);
+        }
+        mNextPageToCount = null;
+        mTotalMatches = 0;
+    }
+
+    /**
+     * Add these search results into the model. Returns true if another search task was started now
+     * that these results have arrived, false if no further searching is necessary.
+     */
+    public boolean updateMatches(String matchesQuery, int page, MatchRects matches) {
+        Preconditions.checkState(
+                getNumPages() >= 0, "updateMatches should only be called after setNumPages");
+
+        String currentQuery = this.mQuery.get();
+        if (!Objects.equals(matchesQuery, currentQuery)) {
+            return false; // This data is irrelevant as it is for an old query - ignore.
+        }
+
+        // Update pageToMatchCount and totalMatches with data from this page, if it is new data.
+        if (mPageToMatchCount[page] == -1) {
+            mPageToMatchCount[page] = matches.size();
+            mTotalMatches += matches.size();
+        }
+
+        // If a search is ongoing and we've found the next match on this page, we update
+        // selectedMatch and stop the search by setting nextSelectedPage iterator to null.
+        if (mNextSelectedPage != null
+                && mNextSelectedPage.hasNext()
+                && mNextSelectedPage.peekNext() == page
+                && !matches.isEmpty()) {
+
+            if (mLastSelectedMatch != null && mLastSelectedMatch.getPage() == page) {
+                // The last search result was on this page too - find the new match closest to
+                // the previous:
+                mSelectedMatch.set(mLastSelectedMatch.nearestMatch(currentQuery, matches));
+            } else {
+                // Select either the first or last match on the page, depending on which direction
+                // we are searching:
+                int selectedIndex =
+                        (mNextSelectedPage.getDirection() == Direction.BACKWARDS) ? matches.size()
+                                - 1 : 0;
+                mSelectedMatch.set(new SelectedMatch(currentQuery, page, matches, selectedIndex));
+            }
+            mLastSelectedMatch = mSelectedMatch.get();
+            // Clear the nextSelectedPage iterator, indicating we have found selected a match.
+            mNextSelectedPage = null;
+        }
+
+        // Search for the next selected match, or if that isn't needed, continue counting matches.
+        boolean newSearchStarted =
+                searchNextPageThat(Condition.IS_MATCH_COUNT_UNKNOWN_OR_POSITIVE, mNextSelectedPage)
+                        || searchNextPageThat(Condition.IS_MATCH_COUNT_UNKNOWN, mNextPageToCount);
+        updateMatchCount();
+        return newSearchStarted;
+    }
+
+    private void updateMatchCount() {
+        SelectedMatch currentMatch = mSelectedMatch.get();
+        int overallSelectedIndex = -1;
+        if (currentMatch != null) {
+            overallSelectedIndex = currentMatch.getSelected();
+            for (int p = 0; p < currentMatch.getPage(); p++) {
+                if (mPageToMatchCount[p] > 0) {
+                    overallSelectedIndex += mPageToMatchCount[p];
+                }
+            }
+        }
+        boolean isAllPagesCounted = mNextPageToCount != null && !mNextPageToCount.hasNext();
+        MatchCount newMatchCount =
+                new MatchCount(overallSelectedIndex, mTotalMatches, isAllPagesCounted);
+        if (!Objects.equals(newMatchCount, mMatchCount.get())) {
+            mMatchCount.set(newMatchCount);
+        }
+    }
+
+    /**
+     * Selects the next match - may succeed immediately, if the next match is on the same page,
+     * or may
+     * request it from the PdfLoader, which will run asynchronously and eventually call {@link
+     * #updateMatches}.
+     */
+    public void selectNextMatch(Direction direction, int viewingPage) {
+        if (getNumPages() < 0) {
+            return; // Cannot search until setNumPages is called.
+        }
+
+        String currentQuery = mQuery.get();
+        SelectedMatch currentMatch = mSelectedMatch.get();
+
+        if (currentQuery != null) {
+            if (selectNextMatchOnPage(direction)) {
+                return;
+            }
+            int startPage = viewingPage;
+            if (currentMatch != null) {
+                startPage = currentMatch.getPage() + direction.sign;
+            }
+            mNextSelectedPage = CycleRange.of(startPage, getNumPages(), direction).iterator();
+            searchNextPageThat(Condition.IS_MATCH_COUNT_UNKNOWN_OR_POSITIVE, mNextSelectedPage);
+        }
+    }
+
+    private boolean selectNextMatchOnPage(Direction direction) {
+        if (mSelectedMatch.get() != null) {
+            SelectedMatch nextMatch = mSelectedMatch.get().selectNextMatchOnPage(direction);
+            if (nextMatch != null) {
+                mSelectedMatch.set(nextMatch);
+                mLastSelectedMatch = nextMatch;
+                updateMatchCount();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Return the page overlay for the selection. */
+    @Nullable
+    public PdfHighlightOverlay getOverlay(String matchesQuery, int page, MatchRects matches) {
+        if (page == getSelectedPage()) {
+            return mSelectedMatch.get().getOverlay();
+        }
+        if (Objects.equals(matchesQuery, mQuery.get())) {
+            return new PdfHighlightOverlay(matches);
+        }
+        return null;
+    }
+
+    /**
+     * Walks through the given iterator, and launches a search task as soon as a page is found that
+     * meets the given condition.
+     *
+     * @return true if such a page is found and a search task is started.
+     */
+    private boolean searchNextPageThat(Condition condition,
+            @Nullable CycleRange.Iterator iterator) {
+        if (iterator == null) {
+            return false;
+        }
+        while (iterator.hasNext() && !condition.apply(mPageToMatchCount[iterator.peekNext()])) {
+            iterator.next();
+        }
+        if (iterator.hasNext()) {
+            mPdfLoader.searchPageText(iterator.peekNext(), mQuery.get());
+            return true;
+        }
+        return false;
+    }
+
+    /** Different conditions relating to the number of matches known to be on a certain page. */
+    private enum Condition {
+        IS_MATCH_COUNT_UNKNOWN {
+            @Override
+            boolean apply(int matchCount) {
+                return matchCount == -1;
+            }
+        },
+        IS_MATCH_COUNT_UNKNOWN_OR_POSITIVE {
+            @Override
+            boolean apply(int matchCount) {
+                return matchCount != 0;
+            }
+        };
+
+        abstract boolean apply(int matchCount);
+    }
+
+    /**
+     * Treat whitespace-only strings the same as null, which means, don't search: whitespace can
+     * match
+     * newlines, which don't have a highlightable area.
+     */
+    @Nullable
+    public static String whiteSpaceToNull(String query) {
+        return (query != null && TextUtils.isGraphic(query)) ? query : null;
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java
new file mode 100644
index 0000000..1741885
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer;
+
+import android.graphics.Rect;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.aidl.MatchRects;
+import androidx.pdf.util.CycleRange.Direction;
+import androidx.pdf.util.Preconditions;
+
+/**
+ * Represents a currently selected match, including the query that was matched, the page the match
+ * is on, all the matches on that page, and which of these matches on the page is currently
+ * selected.
+ *
+ * <p>If no match is selected, then a null is used instead of a SelectedMatch.
+ *
+ * <p>Immutable.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SelectedMatch {
+    private final String mQuery;
+    private final int mPage;
+    private final MatchRects mPageMatches;
+    private final int mSelected;
+
+    /**
+     * Construct a new immutable SelectedMatch - either one with at least one match, where one of
+     * the
+     * matches is selected, or one with no matches.
+     */
+    SelectedMatch(String query, int page, MatchRects pageMatches, int selected) {
+        Preconditions.checkNotNull(query);
+        Preconditions.checkNotNull(pageMatches);
+        Preconditions.checkArgument(!pageMatches.isEmpty(), "Cannot select empty matches");
+        Preconditions.checkArgument(
+                selected >= 0 && selected < pageMatches.size(), "selected match is out of range");
+
+        this.mQuery = query;
+        this.mPage = page;
+        this.mPageMatches = pageMatches;
+        this.mSelected = selected;
+    }
+
+    public String getQuery() {
+        return mQuery;
+    }
+
+    public int getPage() {
+        return mPage;
+    }
+
+    public MatchRects getPageMatches() {
+        return mPageMatches;
+    }
+
+    public int getSelected() {
+        return mSelected;
+    }
+
+    public boolean isEmpty() {
+        return mPageMatches.isEmpty();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof SelectedMatch)) {
+            return false;
+        }
+        SelectedMatch that = (SelectedMatch) other;
+        return this.mQuery.equals(that.mQuery)
+                && this.mPage == that.mPage
+                && this.mPageMatches.equals(that.mPageMatches)
+                && this.mSelected == that.mSelected;
+    }
+
+    @Override
+    public int hashCode() {
+        return mQuery.hashCode() + 31 * mPage + 101 * mPageMatches.hashCode() + 313 * mSelected;
+    }
+
+    @Nullable
+    public Rect getFirstSelectionRect() {
+        return isEmpty() ? null : mPageMatches.getFirstRect(mSelected);
+    }
+
+    /** Returns the page overlay for this selection. */
+    @Nullable
+    public PdfHighlightOverlay getOverlay() {
+        return isEmpty() ? null : new PdfHighlightOverlay(mPageMatches, mSelected);
+    }
+
+    @Nullable
+    public SelectedMatch selectNextMatchOnPage(Direction direction) {
+        if (direction == Direction.BACKWARDS && mSelected > 0) {
+            return withSelected(mSelected - 1);
+        } else if (direction == Direction.FORWARDS && mSelected < mPageMatches.size() - 1) {
+            return withSelected(mSelected + 1);
+        }
+        return null;
+    }
+
+    private SelectedMatch withSelected(int selected) {
+        return new SelectedMatch(mQuery, mPage, mPageMatches, selected);
+    }
+
+    /**
+     * Given a new set of matches, selects the one that is closest to the old selected match (if
+     * any).
+     */
+    public SelectedMatch nearestMatch(String newQuery, MatchRects newMatches) {
+        if (newMatches.isEmpty()) {
+            return noMatches(newQuery, mPage);
+        }
+        if (this.isEmpty()) {
+            return firstMatch(newQuery, mPage, newMatches);
+        }
+
+        int oldCharIndex = isEmpty() ? 0 : mPageMatches.getCharIndex(mSelected);
+        int newMatch = newMatches.getMatchNearestCharIndex(oldCharIndex);
+        return new SelectedMatch(newQuery, mPage, newMatches, newMatch);
+    }
+
+    /** Returns a SelectedMatch that contains no matches and so nothing is selected. */
+    public static SelectedMatch noMatches(String query, int page) {
+        return new SelectedMatch(query, page, MatchRects.NO_MATCHES, -1);
+    }
+
+    /** Selects the first match from the given matches. */
+    public static SelectedMatch firstMatch(String query, int page, MatchRects matches) {
+        return matches.isEmpty() ? noMatches(query, page) : new SelectedMatch(query, page, matches,
+                0);
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
index 9171f45..d76d842 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
@@ -33,8 +33,6 @@
 import androidx.pdf.util.Observables;
 import androidx.pdf.util.Observables.ExposedValue;
 
-import java.util.Map;
-
 /**
  * A widget that displays the contents of a file in a given PDF format.
  *
@@ -56,7 +54,7 @@
  * themselves - see {@link LoadingViewer} which handles some of this.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressWarnings("UnusedVariable")
+@SuppressWarnings("deprecation")
 public abstract class Viewer extends Fragment {
 
     protected abstract String getLogTag();
@@ -124,20 +122,9 @@
     // Debug log of lifecycle events that happened on this viewer, helps investigating.
     private final StringBuilder mEventlog = new StringBuilder();
 
-    // Debug counters to track instance allocation.
-    private static final Map<String, Integer> INSTANCE_COUNTS;
-
-    private final int mInstanceCount;
-
-    static {
-        INSTANCE_COUNTS = null;
-    }
-
     {
         // We can call getArguments() from setters and know that it will not be null.
         setArguments(new Bundle());
-
-        mInstanceCount = 0;
     }
 
     /** Reports the {@link ViewState} of this Fragment. */
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
index 7fb5e04..a37b159 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
@@ -338,7 +338,7 @@
     class LoadDocumentTask extends AbstractPdfTask<PdfStatus> {
         private final String mPassword;
         private int mNumPages;
-        private boolean mIsLinearized;
+//        private boolean mIsLinearized;
 
         LoadDocumentTask() {
             this(null);
@@ -382,7 +382,7 @@
 
             if (result == PdfStatus.LOADED) {
                 mNumPages = pdfDocument.getPdfDocumentRemote().numPages();
-                mIsLinearized = pdfDocument.getPdfDocumentRemote().isPdfLinearized();
+//                mIsLinearized = pdfDocument.getPdfDocumentRemote().isPdfLinearized();
             }
             return result;
         }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java
new file mode 100644
index 0000000..95d4bde
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer.password;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnShowListener;
+import android.graphics.PorterDuff;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.fragment.app.DialogFragment;
+import androidx.pdf.R;
+import androidx.pdf.util.Accessibility;
+
+/**
+ * Dialog for querying password for a protected file. The dialog has 2 buttons:
+ * <ul>
+ * <li>Exit, exits the application,
+ * <li>Open, tries to open the document with the given password. If this is not successful, the
+ *     dialog stays up, and offers to try again (the controller should call {@link #retry}).
+ *     If successful, the controller should call {@link #dismiss}.
+ * </ul>
+ * <p>
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings("deprecation")
+public abstract class PasswordDialog extends DialogFragment {
+
+    private int mTextDefaultColor;
+    private int mBlueColor;
+    private int mTextErrorColor;
+    private AlertDialog mPasswordDialog;
+
+    private boolean mIncorrect;
+    private boolean mFinishOnCancel;
+
+    /**
+     * @param finishOnCancel being true indicates that the activity will be killed when the user
+     *                       presses the cancel button on this dialog.
+     */
+    public void setFinishOnCancel(boolean finishOnCancel) {
+        this.mFinishOnCancel = finishOnCancel;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        LayoutInflater inflater = getActivity().getLayoutInflater();
+        View view = inflater.inflate(R.layout.dialog_password, null);
+        builder.setTitle(R.string.title_dialog_password)
+                .setView(view)
+                .setPositiveButton(R.string.button_open, null)
+                .setNegativeButton(R.string.button_cancel, null);
+        final AlertDialog dialog = builder.create();
+
+        dialog.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+
+        final EditText passwordField = (EditText) view.findViewById(R.id.password);
+        setupPasswordField(passwordField);
+
+        // Hijack the positive button to NOT dismiss the dialog immediately.
+        dialog.setOnShowListener(
+                new OnShowListener() {
+                    @Override
+                    public void onShow(DialogInterface useless) {
+                        // TODO: Track password prompt displayed.
+                        final Button open = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+                        final Button exit = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
+
+                        open.setOnClickListener(
+                                new OnClickListener() {
+                                    @Override
+                                    public void onClick(View v) {
+                                        sendPassword(passwordField);
+                                        // TODO: Track password prompt opened.
+                                    }
+                                });
+
+                        exit.setOnClickListener(
+                                new OnClickListener() {
+                                    @Override
+                                    public void onClick(View v) {
+                                        dialog.cancel();
+                                        // TODO: Track password prompt exit.
+                                    }
+                                });
+
+                        // Clear red patches on new text
+                        passwordField.addTextChangedListener(
+                                new TextWatcher() {
+                                    @Override
+                                    public void onTextChanged(CharSequence s, int start, int before,
+                                            int count) {
+                                        if (mIncorrect) {
+                                            clearIncorrect();
+                                        }
+                                    }
+
+                                    @Override
+                                    public void beforeTextChanged(CharSequence s, int start,
+                                            int count, int after) {
+                                    }
+
+                                    @Override
+                                    public void afterTextChanged(Editable s) {
+                                    }
+                                });
+                    }
+                });
+
+        mPasswordDialog = dialog;
+        return dialog;
+    }
+
+    private void setupPasswordField(final EditText passwordField) {
+        passwordField.setFocusable(true);
+        passwordField.requestFocus();
+        // Do not expand the text field to full screen when in landscape.
+        passwordField.setImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+
+        // Set the open button text with title case.
+        String openText = getResources().getString(R.string.button_open);
+        passwordField.setImeActionLabel(openText, EditorInfo.IME_ACTION_DONE);
+
+        // Handle 'Enter'
+        passwordField.setOnKeyListener(new OnKeyListener() {
+            @Override
+            public boolean onKey(View v, int keyCode, KeyEvent event) {
+                if (keyCode == KeyEvent.KEYCODE_ENTER) {
+                    sendPassword(passwordField);
+                    return true;
+                }
+                return false;
+            }
+        });
+
+        // Handle soft keyboard "Done" button.
+        passwordField.setOnEditorActionListener(new OnEditorActionListener() {
+            @Override
+            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+                if (actionId == EditorInfo.IME_ACTION_DONE) {
+                    sendPassword(passwordField);
+                    return true;
+                }
+                return false;
+            }
+        });
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mTextDefaultColor = getResources().getColor(R.color.text_default);
+        mTextErrorColor = getResources().getColor(R.color.text_error);
+        mBlueColor = getResources().getColor(R.color.google_blue);
+
+        EditText textField = (EditText) getDialog().findViewById(R.id.password);
+        textField.getBackground().setColorFilter(mBlueColor, PorterDuff.Mode.SRC_ATOP);
+
+        mPasswordDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(mBlueColor);
+        mPasswordDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(mBlueColor);
+
+        showSoftKeyboard(textField);
+    }
+
+    private void showSoftKeyboard(View view) {
+        if (view.requestFocus()) {
+            InputMethodManager imm = (InputMethodManager)
+                    getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+            imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
+        }
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        if (mFinishOnCancel) {
+            getActivity().finish();
+        } else {
+            dismiss();
+            showErrorOnDialogCancel();
+        }
+    }
+
+    /** Set the password input by the user. */
+    public abstract void sendPassword(EditText textField);
+
+    /** Show error when user cancels password prompt dialog. */
+    public abstract void showErrorOnDialogCancel();
+
+    /** The given password didn't work, perhaps try again? */
+    public void retry() {
+        // TODO: Track incorrect password input.
+        mIncorrect = true;
+        EditText textField = (EditText) getDialog().findViewById(R.id.password);
+        textField.selectAll();
+
+        swapBackground(textField, false);
+        textField.getBackground().setColorFilter(mTextErrorColor, PorterDuff.Mode.SRC_ATOP);
+
+        TextView label = (TextView) getDialog().findViewById(R.id.label);
+        label.setText(R.string.label_password_incorrect);
+        label.setTextColor(mTextErrorColor);
+
+        Accessibility.get().announce(getActivity(), getDialog().getCurrentFocus(),
+                R.string.desc_password_incorrect_message);
+
+        getDialog().findViewById(R.id.password_alert).setVisibility(View.VISIBLE);
+    }
+
+    private void clearIncorrect() {
+        mIncorrect = false;
+        TextView label = (TextView) getDialog().findViewById(R.id.label);
+        label.setText(R.string.label_password_first);
+        label.setTextColor(mTextDefaultColor);
+
+        EditText textField = (EditText) getDialog().findViewById(R.id.password);
+        swapBackground(textField, true);
+
+        getDialog().findViewById(R.id.password_alert).setVisibility(View.GONE);
+    }
+
+    private void swapBackground(EditText textField, boolean reverse) {
+        if (!reverse) {
+            textField.setBackground(
+                    getResources().getDrawable(R.drawable.textfield_default_mtrl_alpha));
+        } else {
+            EditText sample = new EditText(getActivity());
+            textField.setBackground(sample.getBackground());
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollContentModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollContentModel.java
new file mode 100644
index 0000000..093fdda
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollContentModel.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.widget;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Model for a {@link FastScrollView} to update the scrollbar when the content view is scrolled
+ * and update the content scroll position when the scroll thumb is dragged.
+ * <p>
+ * All units used are arbitrary but must be consistent. For example they could be pixels or page
+ * numbers.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface FastScrollContentModel {
+
+    /**
+     * Estimate the full content height.
+     */
+    float estimateFullContentHeight();
+
+    /**
+     * The currently visible height in the {@link FastScrollView} in the same units as
+     * {@link #estimateFullContentHeight}.
+     */
+    float visibleHeight();
+
+    /**
+     * Scroll the content view to the position indicated.
+     *
+     * @param position the latest/current scroll position
+     * @param stable   whether the scroll gesture is finished (stable is false while the gesture
+     *                 is in
+     *                 progress, and is true when the gesture finishes).
+     */
+    void fastScrollTo(float position, boolean stable);
+
+    /**
+     * Allow the {@link FastScrollView} to update the scroll thumb when the content scrolls.
+     */
+    void setFastScrollListener(FastScrollListener listener);
+
+    /**
+     * Listener for content scroll events in units consistent with the other methods in this class.
+     */
+    interface FastScrollListener {
+
+        /** Update scrollbar based on the given position. */
+        void updateFastScrollbar(float position);
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java
new file mode 100644
index 0000000..da3660f
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.R;
+import androidx.pdf.util.MathUtils;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.ObservableValue.ValueObserver;
+import androidx.pdf.util.Observables;
+import androidx.pdf.widget.FastScrollContentModel.FastScrollListener;
+
+/**
+ * A {@link FrameLayout} that draws a draggable scrollbar over its child views. It uses a
+ * {@link FastScrollContentModel} as its model to listen for scroll events on the content view to
+ * control the scrollbar and conversely to scroll the content view when the scrollbar thumb is
+ * dragged.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class FastScrollView extends FrameLayout implements FastScrollListener {
+
+    private enum State {
+        NONE,
+        VISIBLE,
+        DRAG
+    }
+
+    private static final long FADE_DELAY_MS = 1300;
+    private static final float MIN_SCREENS_TO_SHOW = 1.5f;
+
+    private final View mDragHandle;
+    private final float mOriginalTranslateX;
+
+    private FastScrollContentModel mScrollable;
+    private Observables.ExposedValue<Integer> mThumbY = Observables.newExposedValueWithInitialValue(
+            0);
+    private float mCurrentPosition;
+    private State mState = State.NONE;
+    private boolean mShowScrollThumb = false;
+    private final int mScrollbarMarginDefault;
+
+    // The track's top and bottom margin include space for the scroll-thumb, but
+    // this isn't included in the scrollBar margin as specified by callers.
+    private int mTrackTopMargin;
+    private int mTrackRightMargin;
+    private int mTrackBottomMargin;
+
+    /** Has the thumb been dragged during the display of the scrollbar */
+    private boolean mDragged;
+
+    private final ValueObserver<Integer> mYObserver =
+            new ValueObserver<Integer>() {
+                @Override
+                public void onChange(@Nullable Integer oldValue, @Nullable Integer newValue) {
+                    View view = mDragHandle;
+                    if (view != null && newValue != null) {
+                        int transY = newValue - (view.getMeasuredHeight() / 2);
+                        view.setTranslationY(transY);
+                    }
+                }
+            };
+
+    public FastScrollView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FastScrollView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        setWillNotDraw(false);
+
+        mDragHandle = LayoutInflater.from(context).inflate(R.layout.fastscroll_handle, this, false);
+        mDragHandle.setAlpha(0F);
+        mOriginalTranslateX = mDragHandle.getTranslationX();
+
+        Resources res = getContext().getResources();
+        mScrollbarMarginDefault = res.getDimensionPixelOffset(
+                R.dimen.viewer_fastscroll_edge_offset);
+
+        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.FastScrollView, 0,
+                0);
+        setScrollbarMarginTop(ta.getDimensionPixelOffset(
+                R.styleable.FastScrollView_scrollbarMarginTop, mScrollbarMarginDefault));
+        setScrollbarMarginBottom(ta.getDimensionPixelOffset(
+                R.styleable.FastScrollView_scrollbarMarginBottom, mScrollbarMarginDefault));
+        ta.recycle();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        addView(mDragHandle, getChildCount()); // Add to end so that we draw on top.
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        Integer y = mThumbY.get();
+        if (y != null) {
+            mYObserver.onChange(null, y);
+        }
+        mThumbY.addObserver(mYObserver);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mThumbY.removeObserver(mYObserver);
+    }
+
+    public void setScrollbarMarginTop(int scrollbarMarginTop) {
+        this.mTrackTopMargin = scrollbarMarginTop + mDragHandle.getMeasuredHeight() / 2;
+    }
+
+    public void setScrollbarMarginRight(int scrollbarMarginRight) {
+        // This view does not support RTL so there is no reason to expose left
+        this.mTrackRightMargin = scrollbarMarginRight;
+    }
+
+    public void setScrollbarMarginBottom(int scrollbarMarginBottom) {
+        this.mTrackBottomMargin = scrollbarMarginBottom + mDragHandle.getMeasuredHeight() / 2;
+    }
+
+    private void updateDragHandleX() {
+        // This has to be calculated because the amount of reserve space on the right can change
+        // mattering on display configuration. The FastScrollView also holds other views, as it is a
+        // ViewGroup, and we want the other views to ignore the reserve space.
+        mDragHandle.setX(
+                (getMeasuredWidth() + mOriginalTranslateX)
+                        - (mDragHandle.getMeasuredWidth() + mTrackRightMargin));
+    }
+
+    /** Set listener on scrollable. */
+    public void setScrollable(FastScrollContentModel scrollable) {
+        this.mScrollable = scrollable;
+        scrollable.setFastScrollListener(this);
+    }
+
+    /** Return the Y coordinate of center of the Scroller thumb, in pixels. */
+    public ObservableValue<Integer> getScrollerPositionY() {
+        return mThumbY;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mState != State.NONE && ev.getAction() == MotionEvent.ACTION_DOWN) {
+            if (isPointInside(ev.getX(), ev.getY())) {
+                setState(State.DRAG);
+                return true;
+            }
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    /** Set view as visible. */
+    public void setVisible() {
+        mDragHandle.setAlpha(1);
+        mDragHandle.animate().setStartDelay(FADE_DELAY_MS).alpha(0F).start();
+    }
+
+    private void setState(State state) {
+        switch (state) {
+            case NONE:
+                mDragHandle.setAlpha(0F);
+                if (mDragged) {
+                    // TODO: Tracker fast scroll.
+                    mDragged = false;
+                }
+                break;
+            case VISIBLE:
+                setVisible();
+                break;
+            case DRAG:
+                mDragHandle.animate().alpha(1).start();
+                mDragged = true;
+                break;
+        }
+        this.mState = state;
+        refreshDrawableState();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        // This is due to the different events ordering when keyboard is opened or
+        // retracted vs rotate. Hence to avoid corner cases we just disable the
+        // scroller when size changed, and wait until the scroll position is recomputed
+        // before showing it back.
+        setState(State.NONE);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        updateDragHandleX();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent me) {
+        if (mState == State.NONE) {
+            return false;
+        }
+
+        int action = me.getAction();
+        if (action == MotionEvent.ACTION_DOWN) {
+            if (isPointInside(me.getX(), me.getY())) {
+                // This fixes a bug where an ancestor would capture events like horizontal
+                // fling/scroll, despite the user not having lifted their finger while/after
+                // dragging the scroll bar.
+                this.requestDisallowInterceptTouchEvent(true);
+                setState(State.DRAG);
+                scrollTo((int) me.getY(), true);
+
+                return true;
+            }
+        } else if (action == MotionEvent.ACTION_UP) {
+            if (mState == State.DRAG) {
+                setState(State.VISIBLE);
+                scrollTo((int) me.getY(), true);
+
+                this.requestDisallowInterceptTouchEvent(false);
+
+                return true;
+            }
+        } else if (action == MotionEvent.ACTION_MOVE) {
+            if (mState == State.DRAG) {
+                scrollTo((int) me.getY(), false);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean scrollTo(int newThumbY, boolean stable) {
+        int top = mTrackTopMargin;
+        int bottom = getHeight() - mTrackBottomMargin;
+        newThumbY = MathUtils.clamp(newThumbY, top, bottom);
+        if (!stable && Math.abs(mThumbY.get() - newThumbY) < 2) {
+            return false;
+        }
+        mThumbY.set(newThumbY);
+        int scrollbarLength = bottom - top;
+        float fraction = (mThumbY.get() - mTrackTopMargin) / (float) scrollbarLength;
+        float scrollRange = mScrollable.estimateFullContentHeight() - mScrollable.visibleHeight();
+        mScrollable.fastScrollTo(scrollRange * fraction, stable);
+        return true;
+    }
+
+    boolean isPointInside(float x, float y) {
+        return x > mDragHandle.getX()
+                // Deliberately ignore (x < getWidth() - scrollbarMarginRight) to make it easier
+                // to grab it.
+                && y >= mThumbY.get() - mDragHandle.getMeasuredHeight() / 2
+                && y <= mThumbY.get() + mDragHandle.getMeasuredHeight() / 2;
+    }
+
+
+    @Override
+    public void updateFastScrollbar(float position) {
+        if (position == mCurrentPosition) {
+            return;
+        }
+        mCurrentPosition = position;
+
+        mShowScrollThumb =
+                mScrollable.estimateFullContentHeight()
+                        > mScrollable.visibleHeight() * MIN_SCREENS_TO_SHOW;
+        if (!mShowScrollThumb) {
+            if (mState != State.NONE) {
+                setState(State.NONE);
+            }
+            return;
+        }
+
+        if (mState != State.DRAG) {
+            int scrollbarBottom = getHeight() - mTrackBottomMargin;
+            int scrollbarLength = scrollbarBottom - mTrackTopMargin;
+            float scrollRange =
+                    mScrollable.estimateFullContentHeight() - mScrollable.visibleHeight();
+            int tempThumbY = mTrackTopMargin + (int) (scrollbarLength * position / scrollRange);
+            mThumbY.set(
+                    MathUtils.clamp(tempThumbY, mTrackTopMargin, getHeight() - mTrackBottomMargin));
+            if (mState != State.VISIBLE) {
+                setState(State.VISIBLE);
+            }
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ReusableToast.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ReusableToast.java
index 9bd3cf0..ce711aa 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ReusableToast.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ReusableToast.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/SearchEditText.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/SearchEditText.java
new file mode 100644
index 0000000..9d00051
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/SearchEditText.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * EditText for search queries which shows/hides the keyboard on focus change.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SearchEditText extends EditText {
+    private static final String TAG = SearchEditText.class.getSimpleName();
+
+    /**
+     * Runnable to show the keyboard asynchronously in case the UI is not fully initialized.
+     * Based on
+     * the platform SearchView.
+     */
+    private final Runnable mShowImeRunnable =
+            () -> {
+                InputMethodManager imm =
+                        (InputMethodManager) getContext().getSystemService(
+                                Context.INPUT_METHOD_SERVICE);
+                if (imm != null) {
+                    imm.showSoftInput(SearchEditText.this, 0);
+                }
+            };
+
+    public SearchEditText(Context context) {
+        super(context);
+    }
+
+    public SearchEditText(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public SearchEditText(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+        Log.w(TAG, "focused=" + focused + " direction=" + direction);
+        if (focused) {
+            post(mShowImeRunnable);
+        } else {
+            InputMethodManager imm =
+                    (InputMethodManager) getContext().getSystemService(
+                            Context.INPUT_METHOD_SERVICE);
+            if (imm != null) {
+                imm.hideSoftInputFromWindow(getWindowToken(), 0);
+            }
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/TileView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/TileView.java
index 0ddc2c7..20a7440 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/TileView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/TileView.java
@@ -22,11 +22,9 @@
 import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.Point;
-import android.graphics.Rect;
 import android.view.View;
 
 import androidx.annotation.RestrictTo;
-import androidx.pdf.aidl.Dimensions;
 import androidx.pdf.util.ErrorLog;
 import androidx.pdf.util.Preconditions;
 import androidx.pdf.util.TileBoard.TileInfo;
@@ -37,30 +35,24 @@
  * A basic image view that holds one tile (a bitmap that is a part of a larger image).
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressWarnings("UnusedVariable")
 public class TileView extends View {
 
     private static final Point ZERO = new Point();
     private static final Matrix IDENTITY = new Matrix();
 
-    private final Dimensions mSize;
-    private final Rect mBounds;
-
     final TileInfo mTileInfo;
     private Bitmap mBitmap;
 
     public TileView(Context context, TileInfo tileInfo) {
         super(context);
-        this.mSize = tileInfo.getExactSize();
-        mBounds = new Rect(0, 0, mSize.getWidth(), mSize.getHeight());
         this.mTileInfo = tileInfo;
     }
 
-    void setBitmap(TileInfo tileId, Bitmap bitmap) {
-        Preconditions.checkArgument(Objects.equals(tileId, this.mTileInfo),
-                String.format("Got wrong tileId %s : %s", this.mTileInfo, tileId));
+    void setBitmap(TileInfo tileInfo, Bitmap bitmap) {
+        Preconditions.checkArgument(Objects.equals(tileInfo, this.mTileInfo),
+                String.format("Got wrong tileId %s : %s", this.mTileInfo, tileInfo));
         if (this.mBitmap != null) {
-            ErrorLog.log(getLogTag(), "Used tile receiving new bitmap " + tileId);
+            ErrorLog.log(getLogTag(), "Used tile receiving new bitmap " + tileInfo);
         }
         this.mBitmap = bitmap;
         // This View is already properly laid out, but without this requestLayout, it doesn't draw.
@@ -72,10 +64,18 @@
         mBitmap = null;
     }
 
+    boolean hasBitmap() {
+        return mBitmap != null;
+    }
+
     public Point getOffset() {
         return mTileInfo != null ? mTileInfo.getOffset() : ZERO;
     }
 
+    public TileInfo getTileInfo() {
+        return mTileInfo;
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java
index f524c8b..73e6b70 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java
@@ -35,8 +35,8 @@
  * @param <S> The type of the selection that this class observes, updating the
  *            handles whenever it changes.
  */
-@SuppressWarnings("deprecation")
 @RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings("deprecation")
 public abstract class ZoomableSelectionHandles<S> {
     private static final float SCALE_OFFSET = 0.5f;
     private static final float HANDLE_ALPHA = 0.75f;
diff --git a/pdf/pdf-viewer/src/main/res/drawable-hdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png b/pdf/pdf-viewer/src/main/res/drawable-hdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
new file mode 100644
index 0000000..fb242e2
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-hdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-hdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-hdpi/text_alert.png
new file mode 100644
index 0000000..41bd51a
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-hdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-hdpi/textfield_default_mtrl_alpha.9.png b/pdf/pdf-viewer/src/main/res/drawable-hdpi/textfield_default_mtrl_alpha.9.png
new file mode 100644
index 0000000..8b5b757
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-hdpi/textfield_default_mtrl_alpha.9.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-ldpi/textfield_default_mtrl_alpha.9.png b/pdf/pdf-viewer/src/main/res/drawable-ldpi/textfield_default_mtrl_alpha.9.png
new file mode 100644
index 0000000..6f0c57f
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-ldpi/textfield_default_mtrl_alpha.9.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-mdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png b/pdf/pdf-viewer/src/main/res/drawable-mdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
new file mode 100644
index 0000000..019f3dd
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-mdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-mdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-mdpi/text_alert.png
new file mode 100644
index 0000000..8e5c9ab
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-mdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-mdpi/textfield_default_mtrl_alpha.9.png b/pdf/pdf-viewer/src/main/res/drawable-mdpi/textfield_default_mtrl_alpha.9.png
new file mode 100644
index 0000000..455818f
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-mdpi/textfield_default_mtrl_alpha.9.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-night-hdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-night-hdpi/text_alert.png
new file mode 100644
index 0000000..3b1280f
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-night-hdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-night-mdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-night-mdpi/text_alert.png
new file mode 100644
index 0000000..7bc2275
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-night-mdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-night-xhdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-night-xhdpi/text_alert.png
new file mode 100644
index 0000000..5933daa
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-night-xhdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-night-xxhdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-night-xxhdpi/text_alert.png
new file mode 100644
index 0000000..5e96508
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-night-xxhdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-night-xxxhdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-night-xxxhdpi/text_alert.png
new file mode 100644
index 0000000..8ccfe11
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-night-xxxhdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-xhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png b/pdf/pdf-viewer/src/main/res/drawable-xhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
new file mode 100644
index 0000000..e651fd1
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-xhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-xhdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-xhdpi/text_alert.png
new file mode 100644
index 0000000..89ca55f
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-xhdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-xhdpi/textfield_default_mtrl_alpha.9.png b/pdf/pdf-viewer/src/main/res/drawable-xhdpi/textfield_default_mtrl_alpha.9.png
new file mode 100644
index 0000000..b0c74eb
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-xhdpi/textfield_default_mtrl_alpha.9.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-xxhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png b/pdf/pdf-viewer/src/main/res/drawable-xxhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
new file mode 100644
index 0000000..ccc75aa
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-xxhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-xxhdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-xxhdpi/text_alert.png
new file mode 100644
index 0000000..01912c1
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-xxhdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-xxxhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png b/pdf/pdf-viewer/src/main/res/drawable-xxxhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
new file mode 100644
index 0000000..c92252b
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-xxxhdpi/quantum_gm_ic_drag_indicator_gm_grey_24.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable-xxxhdpi/text_alert.png b/pdf/pdf-viewer/src/main/res/drawable-xxxhdpi/text_alert.png
new file mode 100644
index 0000000..3e39ecd
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable-xxxhdpi/text_alert.png
Binary files differ
diff --git a/pdf/pdf-viewer/src/main/res/drawable/custom_edit_text_cursor.xml b/pdf/pdf-viewer/src/main/res/drawable/custom_edit_text_cursor.xml
new file mode 100644
index 0000000..d921c91
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable/custom_edit_text_cursor.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <size android:width="2dp" />
+    <solid android:color="@color/google_blue" />
+</shape>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/drawable/fastscroll_background.xml b/pdf/pdf-viewer/src/main/res/drawable/fastscroll_background.xml
new file mode 100644
index 0000000..922dea5
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/drawable/fastscroll_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+
+    <solid android:color="@android:color/white" />
+</shape>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/layout/dialog_password.xml b/pdf/pdf-viewer/src/main/res/layout/dialog_password.xml
new file mode 100644
index 0000000..0c521b7
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/layout/dialog_password.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:padding="24dp"
+    >
+    <TextView android:id="@+id/label"
+        style="@style/Label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/label_password_first"/>
+    <EditText android:id="@+id/password"
+        style="@style/TextField"
+        android:inputType="textPassword"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/label"
+        android:layout_alignParentLeft="true"
+        android:selectAllOnFocus="true"
+        android:contentDescription="@string/desc_password"
+        android:textCursorDrawable="@drawable/custom_edit_text_cursor"
+        android:importantForAutofill="no"
+        tools:ignore="LabelFor" />
+    <ImageView android:id="@+id/password_alert"
+        android:src="@drawable/text_alert"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/label"
+        android:layout_alignParentRight="true"
+        android:contentDescription="@string/desc_password_incorrect"
+        android:visibility="gone"
+        />
+</RelativeLayout>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/layout/fastscroll_handle.xml b/pdf/pdf-viewer/src/main/res/layout/fastscroll_handle.xml
new file mode 100644
index 0000000..921f4e9
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/layout/fastscroll_handle.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Copyright (C) 2024 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+  -->
+
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="52dp"
+    android:layout_height="52dp"
+    android:background="@drawable/fastscroll_background"
+    android:elevation="4dp"
+    android:importantForAccessibility="no"
+    android:scaleType="center"
+    android:src="@drawable/quantum_gm_ic_drag_indicator_gm_grey_24"
+    android:translationX="18dp" />
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/layout/file_viewer_pdf.xml b/pdf/pdf-viewer/src/main/res/layout/file_viewer_pdf.xml
new file mode 100644
index 0000000..d101458
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/layout/file_viewer_pdf.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Copyright (C) 2024 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+  -->
+<androidx.pdf.widget.FastScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <androidx.pdf.widget.ZoomView
+        android:id="@+id/zoom_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipToPadding="false"
+        android:paddingBottom="@dimen/viewer_doc_padding_y"
+        android:paddingLeft="@dimen/viewer_doc_padding_x"
+        android:paddingRight="@dimen/viewer_doc_padding_x"
+        android:paddingTop="@dimen/viewer_doc_padding_y">
+
+        <FrameLayout
+            android:id="@+id/zoomed_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_gravity="left">
+
+            <androidx.pdf.viewer.PaginatedView
+                android:id="@+id/pdf_view"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="vertical" />
+        </FrameLayout>
+    </androidx.pdf.widget.ZoomView>
+</androidx.pdf.widget.FastScrollView>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values/attrs.xml b/pdf/pdf-viewer/src/main/res/values/attrs.xml
index 706c28a..7716d69 100644
--- a/pdf/pdf-viewer/src/main/res/values/attrs.xml
+++ b/pdf/pdf-viewer/src/main/res/values/attrs.xml
@@ -15,11 +15,21 @@
   -->
 
 <resources>
+    <declare-styleable name="FastScrollView">
+        <attr name="scrollbarMarginTop" format="dimension" />
+        <attr name="scrollbarMarginBottom" format="dimension" />
+    </declare-styleable>
+
     <declare-styleable name="ZoomView">
         <attr name="minZoom" format="float" />
         <attr name="maxZoom" format="float" />
         <attr name="saveState" format="boolean" />
     </declare-styleable>
 
-    <attr name="actionModeSelectAllDrawable" format="reference" />
+    <declare-styleable name="SelectionModeDrawables">
+        <attr name="actionModeSelectAllDrawable" />
+        <attr name="actionModeCutDrawable" />
+        <attr name="actionModeCopyDrawable" />
+        <attr name="actionModePasteDrawable" />
+    </declare-styleable>
 </resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values/colors.xml b/pdf/pdf-viewer/src/main/res/values/colors.xml
index 4398f27..b6d230a 100644
--- a/pdf/pdf-viewer/src/main/res/values/colors.xml
+++ b/pdf/pdf-viewer/src/main/res/values/colors.xml
@@ -15,7 +15,11 @@
   -->
 
 <resources>
+
+    <color name="google_blue">#ff1a73e8</color>
     <color name="google_white">#ffffffff</color>
     <color name="google_grey">#ff3c4043</color>
+    <color name="text_default">#666</color>
+    <color name="text_error">#da4336</color>
     <color name="selection_handles">#00aadd</color>
 </resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/dimensions.xml b/pdf/pdf-viewer/src/main/res/values/dimensions.xml
index 26625e9..b7b38db 100644
--- a/pdf/pdf-viewer/src/main/res/values/dimensions.xml
+++ b/pdf/pdf-viewer/src/main/res/values/dimensions.xml
@@ -16,6 +16,9 @@
   -->
 
 <resources>
+    <dimen name="viewer_doc_additional_top_offset">12dp</dimen>
+    <dimen name="viewer_fastscroll_edge_offset">15dp</dimen>
+    <dimen name="viewer_fastscroll_rounded_corner_radius">2dp</dimen>
     <dimen name="viewer_doc_padding_y">4dp</dimen>
     <dimen name="viewer_doc_padding_x">16dp</dimen>
 </resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/strings.xml b/pdf/pdf-viewer/src/main/res/values/strings.xml
index a8bdea6..2f264aa 100644
--- a/pdf/pdf-viewer/src/main/res/values/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values/strings.xml
@@ -20,6 +20,30 @@
      "file type: Image" [CHAR LIMIT=25] -->
     <string name="desc_file_type">File type</string>
 
+    <!-- Title of the password dialog [CHAR LIMIT=40] -->
+    <string name="title_dialog_password">This file is protected</string>
+
+    <!-- Label of the password field [CHAR LIMIT=40] -->
+    <string name="label_password_first">Password</string>
+
+    <!-- Label of the password field when incorrect (retrying) [CHAR LIMIT=40] -->
+    <string name="label_password_incorrect">Password incorrect</string>
+
+    <!-- Button for the password dialog: Exit (exits the app). [CHAR LIMIT=16] -->
+    <string name="button_cancel">Cancel</string>
+
+    <!-- Button for the password dialog: Open (open the document). [CHAR LIMIT=16] -->
+    <string name="button_open">Open</string>
+
+    <!-- Content description of the password input field [CHAR LIMIT=50] -->
+    <string name="desc_password">password field</string>
+
+    <!-- Content description of the alert icon when password is incorrect [CHAR LIMIT=50] -->
+    <string name="desc_password_incorrect">password incorrect</string>
+
+    <!-- Content description for message announced to accessibility services when the password entered was incorrect [CHAR LIMIT=50] -->
+    <string name="desc_password_incorrect_message">password incorrect</string>
+
     <!-- Label for displaying the current page, e.g. "1 / 500 [CHAR LIMIT=20] -->
     <string name="label_page_single"><xliff:g example="1" id="page">%1$d</xliff:g> / <xliff:g example="500" id="total">%2$d</xliff:g></string>
 
@@ -30,9 +54,50 @@
          the '%' sign instead of the word 'percent', use double percent, '%%'. [CHAR LIMIT=40] -->
     <string name="desc_zoom">zoom <xliff:g example="66" id="first">%1$d</xliff:g> percent</string>
 
+    <!-- Content description for a go-to within a document.
+        The link destination is to another page within the document itself. [CHAR LIMIT=20] -->
+    <string name="desc_goto_link">Go to page <xliff:g example="12" id="page_number">%1$d</xliff:g></string>
+
     <!-- Content description for a page of a paginated document, eg "page 3". [CHAR LIMIT=50] -->
     <string name="desc_page">page <xliff:g example="3" id="page">%1$d</xliff:g></string>
 
+    <!-- Message for instructing the user to select text to create an inline comment anchor. [CHAR LIMIT=45] -->
+    <string name="message_select_text_to_comment">Select text to place your comment</string>
+
+    <!-- Message for instructing the user to tap to create a positional comment anchor. [CHAR LIMIT=40] -->
+    <string name="message_tap_to_comment">Tap an area to comment on</string>
+
+    <!-- Text for an action to cancel an operation. [CHAR LIMIT=20] -->
+    <string name="action_cancel">Cancel</string>
+
+
+    <!-- Error message when file format isn't valid PDF. [CHAR LIMIT=60] -->
+    <string name="error_file_format_pdf">Cannot display PDF ("<xliff:g example="Treasure Island" id="title">%1$s</xliff:g>" is of invalid format)</string>
+
+    <!-- Error message when there is an error on one page. [CHAR LIMIT=60] -->
+    <string name="error_on_page">Cannot display page <xliff:g example="3" id="page">%1$d</xliff:g>
+        (file error)</string>
+
+    <!-- Contents of a snackbar message that is shown when launching annotation mode fails. The user
+       can try again but it is unlikely that will solve the problem. [CHAR LIMIT=100] -->
+    <string name="annotation_mode_failed_to_open">Can\'t load annotation mode for this item.</string>
+
+    <!-- Content description for a web link within a document that has been shortened to only include the domain.
+         The entire URL is too long to read aloud, but the user should know that it is a link and the domain of the webpage that it links to. [CHAR LIMIT=40] -->
+    <string name="desc_web_link_shortened_to_domain">Link: webpage at <xliff:g example="www.example.com" id="destination_domain">%1$s</xliff:g></string>
+
+    <!-- Content description for a web link within a document.
+       The link destination will also be included in the description, so this label only contains the word "link" to give the user context. [CHAR LIMIT=20] -->
+    <string name="desc_web_link">Link: <xliff:g example="www.example.com/page1.html" id="destination">%1$s</xliff:g></string>
+
+    <!-- Content description for an email address link within the document.
+        The email address will also be included in the description, so this label only contains the word "email" to give the user context. [CHAR LIMIT=40] -->
+    <string name="desc_email_link">Email: <xliff:g example="[email protected]" id="email_address">%1$s</xliff:g></string>
+
+    <!-- Content description for a phone number link within the document. "Phone" refers to generic term.
+         The phone number will also be included in the description, so this label only contains the word "phone" to give the user context. [CHAR LIMIT=40] -->
+    <string name="desc_phone_link">Phone: <xliff:g example="012-555-1234" id="phone_number">%1$s</xliff:g></string>
+
     <!-- Content description for the draggable handle at the start of the selected text. [CHAR LIMIT=50] -->
     <string name="desc_selection_start">selection start</string>
 
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/pdf/pdf-viewer/src/main/res/values/styles.xml
new file mode 100644
index 0000000..e752dd2
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/res/values/styles.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="Label">
+        <item name="android:textColor">@color/text_default</item>
+    </style>
+
+    <style name="TextField" />
+
+</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/Dimensions.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/Dimensions.aidl
index 8b1e85c..1ebe1e2 100644
--- a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/Dimensions.aidl
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/Dimensions.aidl
@@ -1,5 +1,4 @@
 package androidx.pdf.aidl;
 
-@JavaOnlyStableParcelable
 @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
-parcelable Dimensions;
\ No newline at end of file
+@JavaOnlyStableParcelable parcelable Dimensions;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/LinkRects.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/LinkRects.aidl
index 9229074..ebdca9a 100644
--- a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/LinkRects.aidl
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/LinkRects.aidl
@@ -1,5 +1,4 @@
 package androidx.pdf.aidl;
 
-@JavaOnlyStableParcelable
 @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
-parcelable LinkRects;
\ No newline at end of file
+@JavaOnlyStableParcelable parcelable LinkRects;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/MatchRects.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/MatchRects.aidl
index 4130e06..243db41 100644
--- a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/MatchRects.aidl
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/MatchRects.aidl
@@ -1,5 +1,4 @@
 package androidx.pdf.aidl;
 
-@JavaOnlyStableParcelable
 @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
-parcelable MatchRects;
\ No newline at end of file
+@JavaOnlyStableParcelable parcelable MatchRects;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PageSelection.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PageSelection.aidl
index e63ccf4..053a22b 100644
--- a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PageSelection.aidl
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PageSelection.aidl
@@ -1,5 +1,4 @@
 package androidx.pdf.aidl;
 
-@JavaOnlyStableParcelable
 @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
-parcelable PageSelection;
\ No newline at end of file
+@JavaOnlyStableParcelable parcelable PageSelection;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/SelectionBoundary.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/SelectionBoundary.aidl
index efcaedb..2cb0a5b 100644
--- a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/SelectionBoundary.aidl
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/SelectionBoundary.aidl
@@ -1,5 +1,4 @@
 package androidx.pdf.aidl;
 
-@JavaOnlyStableParcelable
 @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
-parcelable SelectionBoundary;
\ No newline at end of file
+@JavaOnlyStableParcelable parcelable SelectionBoundary;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/data/RangeTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/data/RangeTest.java
index f0d78f1..e781107 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/data/RangeTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/data/RangeTest.java
@@ -175,8 +175,8 @@
         Range range = new Range(5, 8);
         Range bounds = new Range(0, 300);
         Range exp = range.expand(1, bounds);
-        assertThat(exp.mFirst).isEqualTo(4);
-        assertThat(exp.mLast).isEqualTo(9);
+        assertThat(exp.getFirst()).isEqualTo(4);
+        assertThat(exp.getLast()).isEqualTo(9);
     }
 
     @Test
@@ -184,8 +184,8 @@
         Range range = new Range(2, 8);
         Range bounds = new Range(0, 300);
         Range exp = range.expand(4, bounds);
-        assertThat(exp.mFirst).isEqualTo(0);
-        assertThat(exp.mLast).isEqualTo(12);
+        assertThat(exp.getFirst()).isEqualTo(0);
+        assertThat(exp.getLast()).isEqualTo(12);
     }
 
     @Test
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/find/MatchCountTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/find/MatchCountTest.java
new file mode 100644
index 0000000..6b25e5f
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/find/MatchCountTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.find;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class MatchCountTest {
+    private final MatchCount mMatchCount1 =
+            new MatchCount(2, 10, true);
+    private final MatchCount mMatchCount2 =
+            new MatchCount(3, 12, false);
+    private final MatchCount mMatchCount3 =
+            new MatchCount(2, 10, true);
+
+    @Test
+    public void equals_ComparingEqualObjects() {
+        boolean equals = mMatchCount1.equals(mMatchCount3);
+        assertThat(equals).isTrue();
+        assertThat(mMatchCount1.toString()).isEqualTo("MatchCount(2 of 10, allPagesCounted=true)");
+    }
+
+    @Test
+    public void equals_ComparingUnequalObjects() {
+        boolean equals = mMatchCount1.equals(mMatchCount2);
+        assertThat(equals).isFalse();
+        assertThat(mMatchCount2.toString()).isEqualTo("MatchCount(3 of 12, allPagesCounted=false)");
+    }
+
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java
index 4e0e1820c..bbdc579 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java
@@ -52,7 +52,7 @@
                         R.raw.launcher_pdfviewer);
     }
 
-    // TODO: Fix the flaky test and uncomment
+    // TODO: Fails in the first execution and then passes. Uncomment when fixed
 //    @Test
 //    public void testBitmapTransferWithOutputFileDescriptor() {
 //        int bufferSize = mSourceBitmap.getWidth() * mSourceBitmap.getHeight() * 256;
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/IntentsTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/IntentsTest.java
index a2b1bf5..b079038 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/IntentsTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/IntentsTest.java
@@ -49,6 +49,7 @@
     Intent mIntent;
 
     @Before
+    @SuppressWarnings("deprecation")
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mContext = ((Application) ApplicationProvider.getApplicationContext()).getBaseContext();
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginationModelTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginationModelTest.java
index d7fa692..8997889 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginationModelTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginationModelTest.java
@@ -113,8 +113,8 @@
 
         // First page not covered by window.
         Range range = mPaginationModel.getPagesInWindow(new Range(205, 500), true);
-        assertThat(range.mFirst).isEqualTo(1);
-        assertThat(range.mLast).isEqualTo(2);
+        assertThat(range.getFirst()).isEqualTo(1);
+        assertThat(range.getLast()).isEqualTo(2);
     }
 
     @Test
@@ -137,8 +137,8 @@
         // Only page is partially in view port - return partial page, even if includePartial =
         // false.
         Range range = mPaginationModel.getPagesInWindow(new Range(204, 500), false);
-        assertThat(range.mFirst).isEqualTo(0);
-        assertThat(range.mLast).isEqualTo(0);
+        assertThat(range.getFirst()).isEqualTo(0);
+        assertThat(range.getLast()).isEqualTo(0);
     }
 
     @Test
@@ -150,13 +150,13 @@
 
         // Include partial = false, don't include partial second page.
         Range range = mPaginationModel.getPagesInWindow(new Range(0, 212), false);
-        assertThat(range.mFirst).isEqualTo(0);
-        assertThat(range.mLast).isEqualTo(0);
+        assertThat(range.getFirst()).isEqualTo(0);
+        assertThat(range.getLast()).isEqualTo(0);
 
         // Include partial = true, include partial second page.
         range = mPaginationModel.getPagesInWindow(new Range(0, 212), true);
-        assertThat(range.mFirst).isEqualTo(0);
-        assertThat(range.mLast).isEqualTo(1);
+        assertThat(range.getFirst()).isEqualTo(0);
+        assertThat(range.getLast()).isEqualTo(1);
     }
 
     /**
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchModelTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchModelTest.java
new file mode 100644
index 0000000..5242d1a
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchModelTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer;
+
+import static androidx.pdf.util.CycleRange.Direction.FORWARDS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+
+import androidx.pdf.aidl.MatchRects;
+import androidx.pdf.find.MatchCount;
+import androidx.pdf.viewer.loader.PdfLoader;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@SuppressWarnings("deprecation")
+public class SearchModelTest {
+
+    @Mock
+    private PdfLoader mPdfLoader;
+
+    @Before
+    @SuppressWarnings("deprecation")
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testSearch() {
+        SearchModel searchModel = new SearchModel(mPdfLoader);
+        searchModel.setNumPages(3);
+
+        searchModel.setQuery("query", /*viewingPage=*/ 1);
+        assertThat(searchModel.selectedMatch().get()).isNull();
+        assertThat(searchModel.matchCount().get()).isNull();
+
+        searchModel.updateMatches("query", 1, createMatchRects(2));
+        assertThat(searchModel.selectedMatch().get())
+                .isEqualTo(new SelectedMatch("query", 1, createMatchRects(2), 0));
+        assertThat(searchModel.matchCount().get()).isEqualTo(new MatchCount(0, 2, false));
+
+        searchModel.updateMatches("query", 2, createMatchRects(3));
+        assertThat(searchModel.matchCount().get()).isEqualTo(new MatchCount(0, 5, false));
+
+        searchModel.updateMatches("query", 0, createMatchRects(2));
+        assertThat(searchModel.matchCount().get()).isEqualTo(new MatchCount(2, 7, true));
+
+        searchModel.selectNextMatch(FORWARDS, 1);
+        assertThat(searchModel.selectedMatch().get())
+                .isEqualTo(new SelectedMatch("query", 1, createMatchRects(2), 1));
+        assertThat(searchModel.matchCount().get()).isEqualTo(new MatchCount(3, 7, true));
+    }
+
+    private static MatchRects createMatchRects(int numRects) {
+        List<Rect> rects = new ArrayList<Rect>();
+        List<Integer> matchToRect = new ArrayList<Integer>();
+        List<Integer> charIndexes = new ArrayList<Integer>();
+        for (int i = 0; i < numRects; i++) {
+            rects.add(new Rect(i * 100, i * 100, i * 101, i * 101));
+            matchToRect.add(i);
+            charIndexes.add(i * 10);
+        }
+
+        return new MatchRects(rects, matchToRect, charIndexes);
+    }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchTest.java
new file mode 100644
index 0000000..142c74d
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer;
+
+import static androidx.pdf.util.CycleRange.Direction.BACKWARDS;
+import static androidx.pdf.util.CycleRange.Direction.FORWARDS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+
+import androidx.pdf.aidl.MatchRects;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+
+public class SelectedMatchTest {
+
+    @Test
+    public void testSelectNextAndPrevious() {
+        MatchRects matchRects = createMatchRects(5, 0, 2, 3);
+        assertThat(matchRects.size()).isEqualTo(3);
+
+        SelectedMatch selectedMatch = new SelectedMatch("query", 1, matchRects, 0);
+        assertThat(selectedMatch.getSelected()).isEqualTo(0);
+
+        selectedMatch = selectedMatch.selectNextMatchOnPage(FORWARDS);
+        assertThat(selectedMatch).isNotNull();
+        selectedMatch = selectedMatch.selectNextMatchOnPage(FORWARDS);
+        assertThat(selectedMatch).isNotNull();
+        assertThat(selectedMatch.getSelected()).isEqualTo(2);
+
+        assertThat(selectedMatch.selectNextMatchOnPage(FORWARDS)).isNull();
+
+        selectedMatch = selectedMatch.selectNextMatchOnPage(BACKWARDS);
+        assertThat(selectedMatch).isNotNull();
+        selectedMatch = selectedMatch.selectNextMatchOnPage(BACKWARDS);
+        assertThat(selectedMatch).isNotNull();
+        assertThat(selectedMatch.getSelected()).isEqualTo(0);
+
+        assertThat(selectedMatch.selectNextMatchOnPage(BACKWARDS)).isNull();
+    }
+
+    private static MatchRects createMatchRects(int numRects, Integer... matchToRect) {
+        List<Rect> rects = new ArrayList<Rect>();
+        List<Integer> charIndexes = new ArrayList<Integer>();
+        for (int i = 0; i < numRects; i++) {
+            rects.add(new Rect(i * 100, i * 100, i * 101, i * 101));
+            charIndexes.add(i * 10);
+        }
+
+        return new MatchRects(rects, Arrays.<Integer>asList(matchToRect), charIndexes);
+    }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/widget/MosaicViewTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/widget/MosaicViewTest.java
index a4332d2..e872f88 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/widget/MosaicViewTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/widget/MosaicViewTest.java
@@ -421,6 +421,7 @@
         }
 
         @Override
+        @SuppressWarnings("deprecation")
         public int getOpacity() {
             return 0;
         }
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt
index d4ad1a2..e685b4c 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt
@@ -32,8 +32,25 @@
             SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= minVersion
     }
 
+    fun isRWithMinExtServicesVersion(minVersion: Int): Boolean {
+        return Build.VERSION.SDK_INT == 30 &&
+            SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) >= minVersion
+    }
+
+    // Helper method to determine if version is testable, for APIs that are S+ only
     fun isTestableVersion(minAdServicesVersion: Int, minExtServicesVersion: Int): Boolean {
         return isTPlusWithMinAdServicesVersion(minAdServicesVersion) ||
             isSWithMinExtServicesVersion(minExtServicesVersion)
     }
+
+    // Helper method to determine if version is testable, for APIs that are available on R
+    fun isTestableVersion(
+        minAdServicesVersion: Int,
+        minExtServicesVersionS: Int,
+        minExtServicesVersionR: Int
+    ): Boolean {
+        return isTPlusWithMinAdServicesVersion(minAdServicesVersion) ||
+            isSWithMinExtServicesVersion(minExtServicesVersionS) ||
+            isRWithMinExtServicesVersion(minExtServicesVersionR)
+    }
 }
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
index 85f68b3..e0d89363 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
@@ -16,6 +16,8 @@
 
 package androidx.privacysandbox.ads.adservices.java.adid
 
+import android.adservices.adid.AdIdManager
+import android.adservices.common.AdServicesOutcomeReceiver
 import android.content.Context
 import android.os.Looper
 import android.os.OutcomeReceiver
@@ -50,17 +52,20 @@
 class AdIdManagerFuturesTest {
 
     private var mSession: StaticMockitoSession? = null
-    private val mValidAdExtServicesSdkExtVersion = VersionCompatUtil.isSWithMinExtServicesVersion(9)
+    private val mValidAdExtServicesSdkExtVersionS =
+        VersionCompatUtil.isSWithMinExtServicesVersion(9)
+    private val mValidAdExtServicesSdkExtVersionR =
+        VersionCompatUtil.isRWithMinExtServicesVersion(11)
 
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
 
-        if (mValidAdExtServicesSdkExtVersion) {
+        if (mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR) {
             // setup a mockitoSession to return the mocked manager
             // when the static method .get() is called
             mSession = ExtendedMockito.mockitoSession()
-                .mockStatic(android.adservices.adid.AdIdManager::class.java)
+                .mockStatic(AdIdManager::class.java)
                 .startMocking()
         }
     }
@@ -73,10 +78,11 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testAdIdOlderVersions() {
-        Assume.assumeFalse("maxSdkVersion = API 33 ext 3 or API 31/32 ext 8",
+        Assume.assumeFalse("maxSdkVersion = API 33 ext 3 or API 31/32 ext 8 or API 30 ext 10",
             VersionCompatUtil.isTestableVersion(
                 /* minAdServicesVersion=*/ 4,
-                /* minExtServicesVersion=*/ 9))
+                /* minExtServicesVersionS=*/ 9,
+                /* minExtServicesVersionR=*/ 11))
         Truth.assertThat(AdIdManagerFutures.from(mContext)).isEqualTo(null)
     }
 
@@ -85,10 +91,19 @@
         Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
             VersionCompatUtil.isTestableVersion(
                 /* minAdServicesVersion= */ 4,
-                /* minExtServicesVersion=*/ 9))
+                /* minExtServicesVersionS=*/ 9,
+                /* minExtServicesVersionR=*/ 11))
 
-        val adIdManager = mockAdIdManager(mContext, mValidAdExtServicesSdkExtVersion)
-        setupResponse(adIdManager)
+        val adIdManager = mockAdIdManager(
+            mContext,
+            mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR
+        )
+
+        when (mValidAdExtServicesSdkExtVersionR) {
+            true -> setupResponseR(adIdManager)
+            false -> setupResponseSPlus(adIdManager)
+        }
+
         val managerCompat = AdIdManagerFutures.from(mContext)
 
         // Actually invoke the compat code.
@@ -97,11 +112,10 @@
         // Verify that the result of the compat call is correct.
         verifyResponse(result.get())
 
-        // Verify that the compat code was invoked correctly.
-        Mockito.verify(adIdManager).getAdId(
-            any<Executor>(),
-            any<OutcomeReceiver<android.adservices.adid.AdId, java.lang.Exception>>()
-        )
+        when (mValidAdExtServicesSdkExtVersionR) {
+            true -> verifyOnR(adIdManager)
+            false -> verifyOnSPlus(adIdManager)
+        }
     }
 
     @SdkSuppress(minSdkVersion = 30)
@@ -111,20 +125,20 @@
         private fun mockAdIdManager(
             spyContext: Context,
             isExtServices: Boolean
-        ): android.adservices.adid.AdIdManager {
-            val adIdManager = Mockito.mock(android.adservices.adid.AdIdManager::class.java)
+        ): AdIdManager {
+            val adIdManager = Mockito.mock(AdIdManager::class.java)
             // mock the .get() method if using extServices version, otherwise mock getSystemService
             if (isExtServices) {
-                `when`(android.adservices.adid.AdIdManager.get(any()))
+                `when`(AdIdManager.get(any()))
                     .thenReturn(adIdManager)
             } else {
                 `when`(spyContext.getSystemService(
-                    android.adservices.adid.AdIdManager::class.java)).thenReturn(adIdManager)
+                    AdIdManager::class.java)).thenReturn(adIdManager)
             }
             return adIdManager
         }
 
-        private fun setupResponse(adIdManager: android.adservices.adid.AdIdManager) {
+        private fun setupResponseSPlus(adIdManager: AdIdManager) {
             // Set up the response that AdIdManager will return when the compat code calls it.
             val adId = android.adservices.adid.AdId("1234", false)
             val answer = { args: InvocationOnMock ->
@@ -137,10 +151,43 @@
             Mockito.doAnswer(answer)
                 .`when`(adIdManager).getAdId(
                     any<Executor>(),
-                    any<OutcomeReceiver<android.adservices.adid.AdId, java.lang.Exception>>()
+                    any<OutcomeReceiver<android.adservices.adid.AdId, Exception>>()
                 )
         }
 
+        private fun setupResponseR(adIdManager: AdIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val adId = android.adservices.adid.AdId("1234", false)
+            val answer = { args: InvocationOnMock ->
+                assertNotEquals(Looper.getMainLooper(), Looper.myLooper())
+                val receiver = args.getArgument<
+                    AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>(1)
+                receiver.onResult(adId)
+                null
+            }
+            Mockito.doAnswer(answer)
+                .`when`(adIdManager).getAdId(
+                    any<Executor>(),
+                    any<AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>()
+                )
+        }
+
+        private fun verifyOnR(adIdManager: AdIdManager) {
+            // Verify that the compat code was invoked correctly.
+            Mockito.verify(adIdManager).getAdId(
+                any<Executor>(),
+                any<AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>()
+            )
+        }
+
+        private fun verifyOnSPlus(adIdManager: AdIdManager) {
+            // Verify that the compat code was invoked correctly.
+            Mockito.verify(adIdManager).getAdId(
+                any<Executor>(),
+                any<OutcomeReceiver<android.adservices.adid.AdId, Exception>>()
+            )
+        }
+
         private fun verifyResponse(adId: androidx.privacysandbox.ads.adservices.adid.AdId) {
             Assert.assertEquals("1234", adId.adId)
             Assert.assertEquals(false, adId.isLimitAdTrackingEnabled)
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/FledgeCtsDebuggableTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/FledgeCtsDebuggableTest.java
index fdd7e82..8a0f736 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/FledgeCtsDebuggableTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/FledgeCtsDebuggableTest.java
@@ -68,7 +68,8 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-@SdkSuppress(minSdkVersion = 28) // API 28 is the lowest level supporting device_config used by this test
+@SdkSuppress(
+        minSdkVersion = 28) // API 28 is the lowest level supporting device_config used by this test
 public class FledgeCtsDebuggableTest {
     protected static final Context sContext = ApplicationProvider.getApplicationContext();
     private static final String TAG = "FledgeCtsDebuggableTest";
@@ -88,13 +89,13 @@
     // for ad selection is 10 seconds
     private static final int API_RESPONSE_LONGER_TIMEOUT_SECONDS = 12;
 
-    private static final AdTechIdentifier SELLER = new AdTechIdentifier("performance-fledge"
-            + "-static-5jyy5ulagq-uc.a.run.app");
+    private static final AdTechIdentifier SELLER =
+            new AdTechIdentifier("performance-fledge-static-5jyy5ulagq-uc.a.run.app");
 
-    private static final AdTechIdentifier BUYER_1 = new AdTechIdentifier("performance-fledge"
-            + "-static-5jyy5ulagq-uc.a.run.app");
-    private static final AdTechIdentifier BUYER_2 = new AdTechIdentifier("performance-fledge"
-            + "-static-2-5jyy5ulagq-uc.a.run.app");
+    private static final AdTechIdentifier BUYER_1 =
+            new AdTechIdentifier("performance-fledge-static-5jyy5ulagq-uc.a.run.app");
+    private static final AdTechIdentifier BUYER_2 =
+            new AdTechIdentifier("performance-fledge-static-2-5jyy5ulagq-uc.a.run.app");
 
     private static final AdSelectionSignals AD_SELECTION_SIGNALS =
             new AdSelectionSignals("{\"ad_selection_signals\":1}");
@@ -104,11 +105,10 @@
 
     private static final Map<AdTechIdentifier, AdSelectionSignals> PER_BUYER_SIGNALS =
             new HashMap<>();
+
     static {
-        PER_BUYER_SIGNALS.put(BUYER_1,
-                new AdSelectionSignals("{\"buyer_signals\":1}"));
-        PER_BUYER_SIGNALS.put(BUYER_2,
-                new AdSelectionSignals("{\"buyer_signals\":2}"));
+        PER_BUYER_SIGNALS.put(BUYER_1, new AdSelectionSignals("{\"buyer_signals\":1}"));
+        PER_BUYER_SIGNALS.put(BUYER_2, new AdSelectionSignals("{\"buyer_signals\":2}"));
     }
 
     private static final String VALID_TRUSTED_BIDDING_URI_PATH = "/trusted/biddingsignals/simple";
@@ -137,22 +137,17 @@
     private static final String SELLER_MALFORMED_DECISION_LOGIC_URI_PATH = "/reporting/seller";
     private static final String BUYER_MALFORMED_BIDDING_LOGIC_URI_PATH = "/reporting/buyer";
 
-    private static final AdSelectionConfig DEFAULT_AD_SELECTION_CONFIG = new AdSelectionConfig(
-            SELLER,
-            Uri.parse(
-                    String.format(
-                            "https://%s%s",
-                            SELLER,
-                            SELLER_DECISION_LOGIC_URI_PATH)),
-            Arrays.asList(BUYER_1, BUYER_2),
-            AD_SELECTION_SIGNALS,
-            SELLER_SIGNALS,
-            PER_BUYER_SIGNALS,
-            Uri.parse(
-                    String.format(
-                            "https://%s%s",
-                            SELLER,
-                            SELLER_TRUSTED_SIGNAL_URI_PATH)));
+    private static final AdSelectionConfig DEFAULT_AD_SELECTION_CONFIG =
+            new AdSelectionConfig(
+                    SELLER,
+                    Uri.parse(
+                            String.format("https://%s%s", SELLER, SELLER_DECISION_LOGIC_URI_PATH)),
+                    Arrays.asList(BUYER_1, BUYER_2),
+                    AD_SELECTION_SIGNALS,
+                    SELLER_SIGNALS,
+                    PER_BUYER_SIGNALS,
+                    Uri.parse(
+                            String.format("https://%s%s", SELLER, SELLER_TRUSTED_SIGNAL_URI_PATH)));
 
     private AdSelectionClient mAdSelectionClient;
     private CustomAudienceClient mCustomAudienceClient;
@@ -161,6 +156,7 @@
     public static void configure() {
         TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
 
+        testUtil.enableVerboseLogging();
         testUtil.overrideAdIdKillSwitch(true);
         testUtil.overrideAppSetIdKillSwitch(true);
         testUtil.overrideKillSwitches(true);
@@ -210,10 +206,8 @@
     @Before
     public void setup() throws Exception {
         Assume.assumeTrue(JavaScriptSandbox.isSupported());
-        mAdSelectionClient =
-                new AdSelectionClient(sContext);
-        mCustomAudienceClient =
-                new CustomAudienceClient(sContext);
+        mAdSelectionClient = new AdSelectionClient(sContext);
+        mCustomAudienceClient = new CustomAudienceClient(sContext);
 
         // TODO(b/266725238): Remove/modify once the API rate limit has been adjusted for FLEDGE
         doSleep(DEFAULT_API_RATE_LIMIT_SLEEP_MS);
@@ -224,8 +218,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -253,12 +246,11 @@
                         .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 
         // Assert that ad3 from BUYER_2 is rendered, since it had the highest bid and score
-        Assert.assertEquals(
-                getUri(BUYER_2, AD_URI_PREFIX + "/ad3"), outcome.getRenderUri());
+        Assert.assertEquals(getUri(BUYER_2, AD_URI_PREFIX + "/ad3"), outcome.getRenderUri());
 
         ReportImpressionRequest reportImpressionRequest =
-                new ReportImpressionRequest(outcome.getAdSelectionId(),
-                        DEFAULT_AD_SELECTION_CONFIG);
+                new ReportImpressionRequest(
+                        outcome.getAdSelectionId(), DEFAULT_AD_SELECTION_CONFIG);
 
         // Performing reporting, and asserting that no exception is thrown
         mAdSelectionClient
@@ -271,8 +263,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -287,8 +278,7 @@
                         Uri.parse(
                                 String.format(
                                         "https://%s%s",
-                                        SELLER + "etld_noise",
-                                        SELLER_DECISION_LOGIC_URI_PATH)),
+                                        SELLER + "etld_noise", SELLER_DECISION_LOGIC_URI_PATH)),
                         Arrays.asList(BUYER_1, BUYER_2),
                         AD_SELECTION_SIGNALS,
                         SELLER_SIGNALS,
@@ -296,8 +286,7 @@
                         Uri.parse(
                                 String.format(
                                         "https://%s%s",
-                                        SELLER + "etld_noise",
-                                        SELLER_TRUSTED_SIGNAL_URI_PATH)));
+                                        SELLER + "etld_noise", SELLER_TRUSTED_SIGNAL_URI_PATH)));
 
         // Joining custom audiences, no result to do assertion on. Failures will generate an
         // exception."
@@ -329,8 +318,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -345,8 +333,7 @@
                         Uri.parse(
                                 String.format(
                                         "https://%s%s",
-                                        SELLER + "etld_noise",
-                                        SELLER_DECISION_LOGIC_URI_PATH)),
+                                        SELLER + "etld_noise", SELLER_DECISION_LOGIC_URI_PATH)),
                         Arrays.asList(BUYER_1, BUYER_2),
                         AD_SELECTION_SIGNALS,
                         SELLER_SIGNALS,
@@ -354,8 +341,7 @@
                         Uri.parse(
                                 String.format(
                                         "https://%s%s",
-                                        SELLER + "etld_noise",
-                                        SELLER_TRUSTED_SIGNAL_URI_PATH)));
+                                        SELLER + "etld_noise", SELLER_TRUSTED_SIGNAL_URI_PATH)));
 
         // Joining custom audiences, no result to do assertion on. Failures will generate an
         // exception."
@@ -377,8 +363,7 @@
                         .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 
         // Assert that the ad3 from buyer 2 is rendered, since it had the highest bid and score
-        Assert.assertEquals(
-                getUri(BUYER_2, AD_URI_PREFIX + "/ad3"), outcome.getRenderUri());
+        Assert.assertEquals(getUri(BUYER_2, AD_URI_PREFIX + "/ad3"), outcome.getRenderUri());
 
         ReportImpressionRequest reportImpressionRequest =
                 new ReportImpressionRequest(
@@ -401,21 +386,20 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
 
         CustomAudience customAudience1 = createCustomAudience(BUYER_1, bidsForBuyer1);
 
-        CustomAudience customAudience2 = createCustomAudience(
-                BUYER_2,
-                bidsForBuyer2,
-                getValidActivationTime(),
-                getValidExpirationTime(),
-                BUYER_MALFORMED_BIDDING_LOGIC_URI_PATH);
-
+        CustomAudience customAudience2 =
+                createCustomAudience(
+                        BUYER_2,
+                        bidsForBuyer2,
+                        getValidActivationTime(),
+                        getValidExpirationTime(),
+                        BUYER_MALFORMED_BIDDING_LOGIC_URI_PATH);
 
         // Joining custom audiences, no result to do assertion on. Failures will generate an
         // exception."
@@ -439,12 +423,11 @@
         // Assert that the ad3 from buyer 2 is skipped despite having the highest bid, since it has
         // malformed bidding logic
         // The winner should come from buyer1 with the highest bid i.e. ad2
-        Assert.assertEquals(
-                getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
+        Assert.assertEquals(getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
 
         ReportImpressionRequest reportImpressionRequest =
-                new ReportImpressionRequest(outcome.getAdSelectionId(),
-                        DEFAULT_AD_SELECTION_CONFIG);
+                new ReportImpressionRequest(
+                        outcome.getAdSelectionId(), DEFAULT_AD_SELECTION_CONFIG);
 
         // Performing reporting, and asserting that no exception is thrown
         mAdSelectionClient
@@ -457,8 +440,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -481,22 +463,20 @@
                 .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 
         // Ad Selection will fail due to scoring logic malformed
-        AdSelectionConfig adSelectionConfig = new AdSelectionConfig(
-                SELLER,
-                Uri.parse(
-                        String.format(
-                                "https://%s%s",
-                                SELLER,
-                                SELLER_MALFORMED_DECISION_LOGIC_URI_PATH)),
-                Arrays.asList(BUYER_1, BUYER_2),
-                AD_SELECTION_SIGNALS,
-                SELLER_SIGNALS,
-                PER_BUYER_SIGNALS,
-                Uri.parse(
-                        String.format(
-                                "https://%s%s",
-                                SELLER,
-                                SELLER_TRUSTED_SIGNAL_URI_PATH)));
+        AdSelectionConfig adSelectionConfig =
+                new AdSelectionConfig(
+                        SELLER,
+                        Uri.parse(
+                                String.format(
+                                        "https://%s%s",
+                                        SELLER, SELLER_MALFORMED_DECISION_LOGIC_URI_PATH)),
+                        Arrays.asList(BUYER_1, BUYER_2),
+                        AD_SELECTION_SIGNALS,
+                        SELLER_SIGNALS,
+                        PER_BUYER_SIGNALS,
+                        Uri.parse(
+                                String.format(
+                                        "https://%s%s", SELLER, SELLER_TRUSTED_SIGNAL_URI_PATH)));
 
         Exception selectAdsException =
                 assertThrows(
@@ -513,20 +493,20 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
 
         CustomAudience customAudience1 = createCustomAudience(BUYER_1, bidsForBuyer1);
 
-        CustomAudience customAudience2 = createCustomAudience(
-                BUYER_2,
-                bidsForBuyer2,
-                getValidActivationTime(),
-                getValidExpirationTime(),
-                "/invalid/bidding/logic/uri");
+        CustomAudience customAudience2 =
+                createCustomAudience(
+                        BUYER_2,
+                        bidsForBuyer2,
+                        getValidActivationTime(),
+                        getValidExpirationTime(),
+                        "/invalid/bidding/logic/uri");
 
         // Joining custom audiences, no result to do assertion on. Failures will generate an
         // exception."
@@ -553,8 +533,8 @@
         Assert.assertEquals(getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
 
         ReportImpressionRequest reportImpressionRequest =
-                new ReportImpressionRequest(outcome.getAdSelectionId(),
-                        DEFAULT_AD_SELECTION_CONFIG);
+                new ReportImpressionRequest(
+                        outcome.getAdSelectionId(), DEFAULT_AD_SELECTION_CONFIG);
 
         // Performing reporting, and asserting that no exception is thrown
         mAdSelectionClient
@@ -567,8 +547,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -592,34 +571,33 @@
 
         // Ad Selection will fail due to scoring logic not found, because the URI that is used to
         // fetch scoring logic does not exist
-        AdSelectionConfig adSelectionConfig = new AdSelectionConfig(
-                SELLER,
-                Uri.parse(
-                        String.format(
-                                "https://%s%s",
-                                SELLER,
-                                "/invalid/seller/decision/logic/uri")),
-                Arrays.asList(BUYER_1, BUYER_2),
-                AD_SELECTION_SIGNALS,
-                SELLER_SIGNALS,
-                PER_BUYER_SIGNALS,
-                Uri.parse(
-                        String.format(
-                                "https://%s%s",
-                                SELLER,
-                                SELLER_TRUSTED_SIGNAL_URI_PATH)));
+        AdSelectionConfig adSelectionConfig =
+                new AdSelectionConfig(
+                        SELLER,
+                        Uri.parse(
+                                String.format(
+                                        "https://%s%s",
+                                        SELLER, "/invalid/seller/decision/logic/uri")),
+                        Arrays.asList(BUYER_1, BUYER_2),
+                        AD_SELECTION_SIGNALS,
+                        SELLER_SIGNALS,
+                        PER_BUYER_SIGNALS,
+                        Uri.parse(
+                                String.format(
+                                        "https://%s%s", SELLER, SELLER_TRUSTED_SIGNAL_URI_PATH)));
         Exception selectAdsException =
                 assertThrows(
                         ExecutionException.class,
                         () ->
                                 mAdSelectionClient
                                         .selectAds(adSelectionConfig)
-                                        .get(API_RESPONSE_LONGER_TIMEOUT_SECONDS,
+                                        .get(
+                                                API_RESPONSE_LONGER_TIMEOUT_SECONDS,
                                                 TimeUnit.SECONDS));
         // Sometimes a 400 status code is returned (ISE) instead of the network fetch timing out
         assertThat(
-                selectAdsException.getCause() instanceof TimeoutException
-                        || selectAdsException.getCause() instanceof IllegalStateException)
+                        selectAdsException.getCause() instanceof TimeoutException
+                                || selectAdsException.getCause() instanceof IllegalStateException)
                 .isTrue();
     }
 
@@ -628,8 +606,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -667,12 +644,11 @@
         // Assert that the ad3 from buyer 2 is skipped despite having the highest bid, since it is
         // not activated yet
         // The winner should come from buyer1 with the highest bid i.e. ad2
-        Assert.assertEquals(
-                getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
+        Assert.assertEquals(getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
 
         ReportImpressionRequest reportImpressionRequest =
-                new ReportImpressionRequest(outcome.getAdSelectionId(),
-                        DEFAULT_AD_SELECTION_CONFIG);
+                new ReportImpressionRequest(
+                        outcome.getAdSelectionId(), DEFAULT_AD_SELECTION_CONFIG);
 
         // Performing reporting, and asserting that no exception is thrown
         mAdSelectionClient
@@ -685,8 +661,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -730,12 +705,11 @@
         // Assert that the ad3 from buyer 2 is skipped despite having the highest bid, since it is
         // expired
         // The winner should come from buyer1 with the highest bid i.e. ad2
-        Assert.assertEquals(
-                getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
+        Assert.assertEquals(getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
 
         ReportImpressionRequest reportImpressionRequest =
-                new ReportImpressionRequest(outcome.getAdSelectionId(),
-                        DEFAULT_AD_SELECTION_CONFIG);
+                new ReportImpressionRequest(
+                        outcome.getAdSelectionId(), DEFAULT_AD_SELECTION_CONFIG);
 
         // Performing reporting, and asserting that no exception is thrown
         mAdSelectionClient
@@ -748,19 +722,19 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
 
         CustomAudience customAudience1 = createCustomAudience(BUYER_1, bidsForBuyer1);
-        CustomAudience customAudience2 = createCustomAudience(
-                BUYER_2,
-                bidsForBuyer2,
-                getValidActivationTime(),
-                getValidExpirationTime(),
-                BUYER_BIDDING_LOGIC_URI_PATH + "?delay=" + 5000);
+        CustomAudience customAudience2 =
+                createCustomAudience(
+                        BUYER_2,
+                        bidsForBuyer2,
+                        getValidActivationTime(),
+                        getValidExpirationTime(),
+                        BUYER_BIDDING_LOGIC_URI_PATH + "?delay=" + 5000);
 
         // Joining custom audiences, no result to do assertion on. Failures will generate an
         // exception.
@@ -784,12 +758,11 @@
         // Assert that the ad3 from buyer 2 is skipped despite having the highest bid, since it
         // timed out
         // The winner should come from buyer1 with the highest bid i.e. ad2
-        Assert.assertEquals(
-                getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
+        Assert.assertEquals(getUri(BUYER_1, AD_URI_PREFIX + "/ad2"), outcome.getRenderUri());
 
         ReportImpressionRequest reportImpressionRequest =
-                new ReportImpressionRequest(outcome.getAdSelectionId(),
-                        DEFAULT_AD_SELECTION_CONFIG);
+                new ReportImpressionRequest(
+                        outcome.getAdSelectionId(), DEFAULT_AD_SELECTION_CONFIG);
 
         // Performing reporting, and asserting that no exception is thrown
         mAdSelectionClient
@@ -802,8 +775,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
         List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
@@ -826,29 +798,29 @@
                 .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 
         // Running ad selection and asserting that the outcome is returned in < 10 seconds
-        AdSelectionConfig adSelectionConfig = new AdSelectionConfig(
-                SELLER,
-                Uri.parse(
-                        String.format(
-                                "https://%s%s",
-                                SELLER,
-                                SELLER_DECISION_LOGIC_URI_PATH + "?delay=" + 10000)),
-                Arrays.asList(BUYER_1, BUYER_2),
-                AD_SELECTION_SIGNALS,
-                SELLER_SIGNALS,
-                PER_BUYER_SIGNALS,
-                Uri.parse(
-                        String.format(
-                                "https://%s%s",
-                                SELLER,
-                                SELLER_TRUSTED_SIGNAL_URI_PATH)));
+        AdSelectionConfig adSelectionConfig =
+                new AdSelectionConfig(
+                        SELLER,
+                        Uri.parse(
+                                String.format(
+                                        "https://%s%s",
+                                        SELLER,
+                                        SELLER_DECISION_LOGIC_URI_PATH + "?delay=" + 10000)),
+                        Arrays.asList(BUYER_1, BUYER_2),
+                        AD_SELECTION_SIGNALS,
+                        SELLER_SIGNALS,
+                        PER_BUYER_SIGNALS,
+                        Uri.parse(
+                                String.format(
+                                        "https://%s%s", SELLER, SELLER_TRUSTED_SIGNAL_URI_PATH)));
         Exception selectAdsException =
                 assertThrows(
                         ExecutionException.class,
                         () ->
                                 mAdSelectionClient
                                         .selectAds(adSelectionConfig)
-                                        .get(API_RESPONSE_LONGER_TIMEOUT_SECONDS,
+                                        .get(
+                                                API_RESPONSE_LONGER_TIMEOUT_SECONDS,
                                                 TimeUnit.SECONDS));
         assertThat(selectAdsException.getCause()).isInstanceOf(TimeoutException.class);
     }
@@ -892,8 +864,7 @@
 
     private static TrustedBiddingData getValidTrustedBiddingDataByBuyer(AdTechIdentifier buyer) {
         return new TrustedBiddingData(
-                getValidTrustedBiddingUriByBuyer(buyer),
-                getValidTrustedBiddingKeys());
+                getValidTrustedBiddingUriByBuyer(buyer), getValidTrustedBiddingKeys());
     }
 
     @RequiresApi(26)
@@ -901,9 +872,7 @@
         Duration maxActivationDelayIn =
                 Duration.ofMillis(FLEDGE_CUSTOM_AUDIENCE_MAX_ACTIVATION_DELAY_IN_MS);
 
-        return Instant.now()
-                .truncatedTo(ChronoUnit.MILLIS)
-                .plus(maxActivationDelayIn.dividedBy(2));
+        return Instant.now().truncatedTo(ChronoUnit.MILLIS).plus(maxActivationDelayIn.dividedBy(2));
     }
 
     @RequiresApi(26)
@@ -913,8 +882,7 @@
 
     @RequiresApi(26)
     private Instant getValidActivationTime() {
-        return Instant.now()
-                .truncatedTo(ChronoUnit.MILLIS);
+        return Instant.now().truncatedTo(ChronoUnit.MILLIS);
     }
 
     @RequiresApi(26)
@@ -923,7 +891,6 @@
                 .plus(Duration.ofMillis(FLEDGE_CUSTOM_AUDIENCE_DEFAULT_EXPIRE_IN_MS));
     }
 
-
     /**
      * @param buyer The name of the buyer for this Custom Audience
      * @param bids these bids, are added to its metadata. Our JS logic then picks this value and
@@ -952,21 +919,21 @@
         // Add the bid value to the metadata
         for (int i = 0; i < bids.size(); i++) {
             ads.add(
-                    new AdData(getUri(buyer, AD_URI_PREFIX + "/ad" + (i + 1)),
+                    new AdData(
+                            getUri(buyer, AD_URI_PREFIX + "/ad" + (i + 1)),
                             "{\"bid\":" + bids.get(i) + "}"));
         }
 
         return new CustomAudience.Builder(
-                buyer,
-                buyer + VALID_NAME,
-                getValidDailyUpdateUriByBuyer(buyer),
-                getUri(buyer, biddingLogicUri),
-                ads)
+                        buyer,
+                        buyer + VALID_NAME,
+                        getValidDailyUpdateUriByBuyer(buyer),
+                        getUri(buyer, biddingLogicUri),
+                        ads)
                 .setActivationTime(activationTime)
                 .setExpirationTime(expirationTime)
                 .setUserBiddingSignals(VALID_USER_BIDDING_SIGNALS)
-                .setTrustedBiddingData(
-                        getValidTrustedBiddingDataByBuyer(buyer))
+                .setTrustedBiddingData(getValidTrustedBiddingDataByBuyer(buyer))
                 .build();
     }
 
@@ -979,8 +946,7 @@
 
         public ListenableFuture<Unit> joinCustomAudience(CustomAudience customAudience) {
             JoinCustomAudienceRequest request = new JoinCustomAudienceRequest(customAudience);
-            return mCustomAudienceManager
-                    .joinCustomAudienceAsync(request);
+            return mCustomAudienceManager.joinCustomAudienceAsync(request);
         }
     }
 
@@ -993,8 +959,8 @@
         }
 
         /**
-         *  Invokes the {@code selectAds} method of {@link AdSelectionManager} and
-         *  returns a future with {@link AdSelectionOutcome}
+         * Invokes the {@code selectAds} method of {@link AdSelectionManager} and returns a future
+         * with {@link AdSelectionOutcome}
          */
         public ListenableFuture<AdSelectionOutcome> selectAds(AdSelectionConfig adSelectionConfig)
                 throws Exception {
@@ -1002,8 +968,8 @@
         }
 
         /**
-         * Invokes the {@code reportImpression} method of {@link AdSelectionManager} and returns
-         * a future with Unit
+         * Invokes the {@code reportImpression} method of {@link AdSelectionManager} and returns a
+         * future with Unit
          */
         public ListenableFuture<Unit> reportImpression(ReportImpressionRequest input) {
             return mAdSelectionManager.reportImpressionAsync(input);
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
index 70e098e..c628c3e 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
@@ -42,48 +42,50 @@
         mInstrumentation = instrumentation;
         mTag = tag;
     }
+
     // Run shell command.
     private void runShellCommand(String command) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
             mInstrumentation.getUiAutomation().executeShellCommand(command);
         }
     }
+
     public void overrideKillSwitches(boolean override) {
         if (override) {
-            runShellCommand("setprop debug.adservices.global_kill_switch " + false);
-            runShellCommand("setprop debug.adservices.topics_kill_switch " + false);
+            runShellCommand("device_config put adservices global_kill_switch " + false);
+            runShellCommand("device_config put adservices topics_kill_switch " + false);
         } else {
-            runShellCommand("setprop debug.adservices.global_kill_switch " + null);
-            runShellCommand("setprop debug.adservices.topics_kill_switch " + null);
+            runShellCommand("device_config put adservices global_kill_switch " + null);
+            runShellCommand("device_config put adservices topics_kill_switch " + null);
         }
     }
 
     public void enableEnrollmentCheck(boolean enable) {
-        runShellCommand(
-                "setprop debug.adservices.disable_topics_enrollment_check " + enable);
+        runShellCommand("device_config put adservices disable_topics_enrollment_check " + enable);
     }
 
     // Override the Epoch Period to shorten the Epoch Length in the test.
     public void overrideEpochPeriod(long overrideEpochPeriod) {
         runShellCommand(
-                "setprop debug.adservices.topics_epoch_job_period_ms " + overrideEpochPeriod);
+                "device_config put adservices topics_epoch_job_period_ms " + overrideEpochPeriod);
     }
 
     // Override the Percentage For Random Topic in the test.
     public void overridePercentageForRandomTopic(long overridePercentage) {
         runShellCommand(
-                "setprop debug.adservices.topics_percentage_for_random_topics "
+                "device_config put adservices topics_percentage_for_random_topics "
                         + overridePercentage);
     }
 
     /** Forces JobScheduler to run the Epoch Computation job */
     public void forceEpochComputationJob() {
         runShellCommand(
-                "cmd jobscheduler run -f" + " " + getAdServicesPackageName() + " " + EPOCH_JOB_ID);
+                "cmd jobscheduler run -f " + getAdServicesPackageName() + " " + EPOCH_JOB_ID);
     }
 
     public void overrideConsentManagerDebugMode(boolean override) {
         String overrideStr = override ? "true" : "null";
+        // This flag is only read through system property and not DeviceConfig
         runShellCommand("setprop debug.adservices.consent_manager_debug_mode " + overrideStr);
     }
 
@@ -91,25 +93,25 @@
         String overrideStr = override ? "*" : "null";
         runShellCommand("device_config put adservices ppapi_app_allow_list " + overrideStr);
         runShellCommand("device_config put adservices msmt_api_app_allow_list " + overrideStr);
-        runShellCommand("device_config put adservices ppapi_app_signature_allow_list "
-                + overrideStr);
+        runShellCommand(
+                "device_config put adservices ppapi_app_signature_allow_list " + overrideStr);
         runShellCommand(
                 "device_config put adservices web_context_client_allow_list " + overrideStr);
     }
 
     public void overrideAdIdKillSwitch(boolean override) {
         if (override) {
-            runShellCommand("setprop debug.adservices.adid_kill_switch " + false);
+            runShellCommand("device_config put adservices adid_kill_switch " + false);
         } else {
-            runShellCommand("setprop debug.adservices.adid_kill_switch " + null);
+            runShellCommand("device_config put adservices adid_kill_switch " + null);
         }
     }
 
     public void overrideAppSetIdKillSwitch(boolean override) {
         if (override) {
-            runShellCommand("setprop debug.adservices.appsetid_kill_switch " + false);
+            runShellCommand("device_config put adservices appsetid_kill_switch " + false);
         } else {
-            runShellCommand("setprop debug.adservices.appsetid_kill_switch " + null);
+            runShellCommand("device_config put adservices appsetid_kill_switch " + null);
         }
     }
 
@@ -131,29 +133,35 @@
     // PhFlags will use the default value.
     public void overrideMeasurementKillSwitches(boolean isOverride) {
         String overrideString = isOverride ? "false" : "null";
-        runShellCommand("setprop debug.adservices.global_kill_switch " + overrideString);
-        runShellCommand("setprop debug.adservices.measurement_kill_switch " + overrideString);
-        runShellCommand("setprop debug.adservices.measurement_api_register_source_kill_switch "
-                + overrideString);
-        runShellCommand("setprop debug.adservices.measurement_api_register_trigger_kill_switch "
-                + overrideString);
-        runShellCommand("setprop debug.adservices.measurement_api_register_web_source_kill_switch "
-                + overrideString);
-        runShellCommand("setprop debug.adservices.measurement_api_register_web_trigger_kill_switch "
-                + overrideString);
-        runShellCommand("setprop debug.adservices.measurement_api_delete_registrations_kill_switch "
-                + overrideString);
-        runShellCommand("setprop debug.adservices.measurement_api_status_kill_switch "
-                + overrideString);
+        runShellCommand("device_config put adservices global_kill_switch " + overrideString);
+        runShellCommand("device_config put adservices measurement_kill_switch " + overrideString);
+        runShellCommand(
+                "device_config put adservices measurement_api_register_source_kill_switch "
+                        + overrideString);
+        runShellCommand(
+                "device_config put adservices measurement_api_register_trigger_kill_switch "
+                        + overrideString);
+        runShellCommand(
+                "device_config put adservices measurement_api_register_web_source_kill_switch "
+                        + overrideString);
+        runShellCommand(
+                "device_config put adservices measurement_api_register_web_trigger_kill_switch "
+                        + overrideString);
+        runShellCommand(
+                "device_config put adservices measurement_api_delete_registrations_kill_switch "
+                        + overrideString);
+        runShellCommand(
+                "device_config put adservices measurement_api_status_kill_switch "
+                        + overrideString);
     }
 
     // Override the flag to disable Measurement enrollment check. Setting to 1 disables enforcement.
     public void overrideDisableMeasurementEnrollmentCheck(String val) {
-        runShellCommand("setprop debug.adservices.disable_measurement_enrollment_check " + val);
+        runShellCommand("device_config put adservices disable_measurement_enrollment_check " + val);
     }
 
     public void resetOverrideDisableMeasurementEnrollmentCheck() {
-        runShellCommand("setprop debug.adservices.disable_measurement_enrollment_check null");
+        runShellCommand("device_config put adservices disable_measurement_enrollment_check null");
     }
 
     // Force using bundled files instead of using MDD downloaded files. This helps to make the test
@@ -161,8 +169,7 @@
     public void shouldForceUseBundledFiles(boolean shouldUse) {
         if (shouldUse) {
             runShellCommand("device_config put adservices classifier_force_use_bundled_files true");
-        }
-        else {
+        } else {
             runShellCommand("device_config delete adservices classifier_force_use_bundled_files");
         }
     }
@@ -178,24 +185,27 @@
 
     public void overrideFledgeSelectAdsKillSwitch(boolean override) {
         if (override) {
-            runShellCommand("setprop debug.adservices.fledge_select_ads_kill_switch " + false);
+            runShellCommand("device_config put adservices fledge_select_ads_kill_switch " + false);
         } else {
-            runShellCommand("setprop debug.adservices.fledge_select_ads_kill_switch " + null);
+            runShellCommand("device_config put adservices fledge_select_ads_kill_switch " + null);
         }
     }
 
     public void overrideFledgeCustomAudienceServiceKillSwitch(boolean override) {
         if (override) {
-            runShellCommand("setprop debug.adservices.fledge_custom_audience_service_kill_switch "
-                    + false);
+            runShellCommand(
+                    "device_config put adservices fledge_custom_audience_service_kill_switch "
+                            + false);
         } else {
-            runShellCommand("setprop debug.adservices.fledge_custom_audience_service_kill_switch "
-                    + null);
+            runShellCommand(
+                    "device_config put adservices fledge_custom_audience_service_kill_switch "
+                            + null);
         }
     }
 
     public void overrideSdkRequestPermitsPerSecond(long maxRequests) {
-        runShellCommand("setprop debug.adservices.sdk_request_permits_per_second " + maxRequests);
+        runShellCommand(
+                "device_config put adservices sdk_request_permits_per_second " + maxRequests);
     }
 
     public void disableDeviceConfigSyncForTests(boolean disabled) {
@@ -216,21 +226,23 @@
 
     public void enableAdServiceSystemService(boolean enabled) {
         if (enabled) {
-            runShellCommand("device_config put adservices adservice_system_service_enabled "
-                    + "\"true\"");
+            runShellCommand(
+                    "device_config put adservices adservice_system_service_enabled \"true\"");
         } else {
-            runShellCommand("device_config put adservices adservice_system_service_enabled "
-                    + "\"false\"");
+            runShellCommand(
+                    "device_config put adservices adservice_system_service_enabled \"false\"");
         }
     }
 
     public void enforceFledgeJsIsolateMaxHeapSize(boolean enforce) {
         if (enforce) {
-            runShellCommand("device_config put adservices fledge_js_isolate_enforce_max_heap_size"
-                    + " true");
+            runShellCommand(
+                    "device_config put adservices fledge_js_isolate_enforce_max_heap_size"
+                            + " true");
         } else {
-            runShellCommand("device_config put adservices fledge_js_isolate_enforce_max_heap_size"
-                    + " false");
+            runShellCommand(
+                    "device_config put adservices fledge_js_isolate_enforce_max_heap_size"
+                            + " false");
         }
     }
 
@@ -238,28 +250,35 @@
     // Used to get the package name. Copied over from com.android.adservices.AndroidServiceBinder
     public String getAdServicesPackageName() {
         final Intent intent = new Intent(TOPICS_SERVICE_NAME);
-        List<ResolveInfo> resolveInfos = ApplicationProvider.getApplicationContext()
-                .getPackageManager()
-                .queryIntentServices(intent, PackageManager.MATCH_SYSTEM_ONLY);
+        List<ResolveInfo> resolveInfos =
+                ApplicationProvider.getApplicationContext()
+                        .getPackageManager()
+                        .queryIntentServices(intent, PackageManager.MATCH_SYSTEM_ONLY);
 
         // TODO: b/271866693 avoid hardcoding package names
         if (resolveInfos != null && Build.VERSION.SDK_INT >= 33) {
-            resolveInfos = resolveInfos.stream()
-                    .filter(info ->
-                            !info.serviceInfo.packageName.contains(EXT_SERVICES_PACKAGE_NAME))
-                    .collect(Collectors.toList());
+            resolveInfos =
+                    resolveInfos.stream()
+                            .filter(
+                                    info ->
+                                            !info.serviceInfo.packageName.contains(
+                                                    EXT_SERVICES_PACKAGE_NAME))
+                            .collect(Collectors.toList());
         }
 
         if (resolveInfos == null || resolveInfos.isEmpty()) {
-            Log.e(mTag, "Failed to find resolveInfo for adServices service. Intent action: "
+            Log.e(
+                    mTag,
+                    "Failed to find resolveInfo for adServices service. Intent action: "
                             + TOPICS_SERVICE_NAME);
             return null;
         }
 
         if (resolveInfos.size() > 1) {
-            String str = String.format(
-                    "Found multiple services (%1$s) for the same intent action (%2$s)",
-                    TOPICS_SERVICE_NAME, resolveInfos);
+            String str =
+                    String.format(
+                            "Found multiple services (%1$s) for the same intent action (%2$s)",
+                            TOPICS_SERVICE_NAME, resolveInfos);
             Log.e(mTag, str);
             return null;
         }
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
index c7be1d1..e8033cf 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
@@ -27,19 +27,32 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Assume;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-
 @RunWith(JUnit4.class)
 @SdkSuppress(minSdkVersion = 28) // API 28 required for device_config used by this test
 public class AdIdManagerTest {
     private static final String TAG = "AdIdManagerTest";
-    private TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(),
-            TAG);
+    private TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    @BeforeClass
+    public static void presuite() {
+        TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+        testUtil.disableDeviceConfigSyncForTests(true);
+        testUtil.enableVerboseLogging();
+    }
+
+    @AfterClass
+    public static void postsuite() {
+        TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+        testUtil.disableDeviceConfigSyncForTests(false);
+    }
 
     @Before
     public void setup() throws Exception {
@@ -66,8 +79,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         AdIdManagerFutures adIdManager =
                 AdIdManagerFutures.from(ApplicationProvider.getApplicationContext());
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
index 19cec1b..ac26172 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
@@ -27,8 +27,10 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Assume;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -42,6 +44,18 @@
     private static final String TAG = "AppSetIdManagerTest";
     TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
 
+    @BeforeClass
+    public static void presuite() {
+        TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+        testUtil.disableDeviceConfigSyncForTests(true);
+    }
+
+    @AfterClass
+    public static void postsuite() {
+        TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+        testUtil.disableDeviceConfigSyncForTests(false);
+    }
+
     @Before
     public void setup() throws Exception {
         mTestUtil.overrideAppSetIdKillSwitch(true);
@@ -66,8 +80,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         AppSetIdManagerFutures appSetIdManager =
                 AppSetIdManagerFutures.from(ApplicationProvider.getApplicationContext());
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
index c43168e..de26fa5 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
@@ -36,8 +36,10 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Assume;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -68,6 +70,19 @@
 
     private MeasurementManagerFutures mMeasurementManager;
 
+    @BeforeClass
+    public static void presuite() {
+        TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+        testUtil.disableDeviceConfigSyncForTests(true);
+        testUtil.enableVerboseLogging();
+    }
+
+    @AfterClass
+    public static void postsuite() {
+        TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+        testUtil.disableDeviceConfigSyncForTests(false);
+    }
+
     @Before
     public void setup() throws Exception {
         // To grant access to all pp api app
@@ -109,12 +124,13 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 5,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 5, /* minExtServicesVersion= */ 9));
 
-        assertThat(mMeasurementManager.registerSourceAsync(
-                SOURCE_REGISTRATION_URI,
-                /* inputEvent= */ null).get())
+        assertThat(
+                        mMeasurementManager
+                                .registerSourceAsync(
+                                        SOURCE_REGISTRATION_URI, /* inputEvent= */ null)
+                                .get())
                 .isNotNull();
     }
 
@@ -123,16 +139,14 @@
         // Skip the test if the right SDK extension is not present
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 5,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 5, /* minExtServicesVersion= */ 9));
         // Skip the test if SDK extension 5 is not present.
 
         SourceRegistrationRequest request =
                 new SourceRegistrationRequest.Builder(
-                        Collections.singletonList(SOURCE_REGISTRATION_URI))
+                                Collections.singletonList(SOURCE_REGISTRATION_URI))
                         .build();
-        assertThat(mMeasurementManager.registerSourceAsync(request).get())
-                .isNotNull();
+        assertThat(mMeasurementManager.registerSourceAsync(request).get()).isNotNull();
     }
 
     @Test
@@ -140,8 +154,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 5,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 5, /* minExtServicesVersion= */ 9));
 
         assertThat(mMeasurementManager.registerTriggerAsync(TRIGGER_REGISTRATION_URI).get())
                 .isNotNull();
@@ -152,11 +165,9 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 5,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 5, /* minExtServicesVersion= */ 9));
 
-        WebSourceParams webSourceParams =
-                new WebSourceParams(SOURCE_REGISTRATION_URI, false);
+        WebSourceParams webSourceParams = new WebSourceParams(SOURCE_REGISTRATION_URI, false);
 
         WebSourceRegistrationRequest webSourceRegistrationRequest =
                 new WebSourceRegistrationRequest(
@@ -176,15 +187,12 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 5,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 5, /* minExtServicesVersion= */ 9));
 
-        WebTriggerParams webTriggerParams =
-                new WebTriggerParams(TRIGGER_REGISTRATION_URI, false);
+        WebTriggerParams webTriggerParams = new WebTriggerParams(TRIGGER_REGISTRATION_URI, false);
         WebTriggerRegistrationRequest webTriggerRegistrationRequest =
                 new WebTriggerRegistrationRequest(
-                        Collections.singletonList(webTriggerParams),
-                        DESTINATION);
+                        Collections.singletonList(webTriggerParams), DESTINATION);
 
         assertThat(mMeasurementManager.registerWebTriggerAsync(webTriggerRegistrationRequest).get())
                 .isNotNull();
@@ -200,13 +208,12 @@
 
         DeletionRequest deletionRequest =
                 new DeletionRequest.Builder(
-                        DeletionRequest.DELETION_MODE_ALL,
-                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                                DeletionRequest.DELETION_MODE_ALL,
+                                DeletionRequest.MATCH_BEHAVIOR_DELETE)
                         .setDomainUris(Collections.singletonList(DOMAIN_URI))
                         .setOriginUris(Collections.singletonList(ORIGIN_URI))
                         .build();
-        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get())
-                .isNotNull();
+        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get()).isNotNull();
     }
 
     @Test
@@ -215,20 +222,18 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 5,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 5, /* minExtServicesVersion= */ 9));
 
         DeletionRequest deletionRequest =
                 new DeletionRequest.Builder(
-                        DeletionRequest.DELETION_MODE_ALL,
-                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                                DeletionRequest.DELETION_MODE_ALL,
+                                DeletionRequest.MATCH_BEHAVIOR_DELETE)
                         .setDomainUris(Collections.singletonList(DOMAIN_URI))
                         .setOriginUris(Collections.singletonList(ORIGIN_URI))
                         .setStart(Instant.ofEpochMilli(0))
                         .setEnd(Instant.now())
                         .build();
-        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get())
-                .isNotNull();
+        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get()).isNotNull();
     }
 
     @Test
@@ -237,22 +242,21 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 5,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 5, /* minExtServicesVersion= */ 9));
 
         DeletionRequest deletionRequest =
                 new DeletionRequest.Builder(
-                        DeletionRequest.DELETION_MODE_ALL,
-                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                                DeletionRequest.DELETION_MODE_ALL,
+                                DeletionRequest.MATCH_BEHAVIOR_DELETE)
                         .setDomainUris(Collections.singletonList(DOMAIN_URI))
                         .setOriginUris(Collections.singletonList(ORIGIN_URI))
                         .setStart(Instant.now().plusMillis(1000))
                         .setEnd(Instant.now())
                         .build();
-        Exception exception = assertThrows(
-                ExecutionException.class,
-                () ->
-                mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get());
+        Exception exception =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get());
         assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
     }
 
@@ -261,8 +265,7 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 5,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 5, /* minExtServicesVersion= */ 9));
 
         int result = mMeasurementManager.getMeasurementApiStatusAsync().get();
         assertThat(result).isEqualTo(1);
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
index 9b46fff5..cb2a953 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
@@ -29,8 +29,10 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Assume;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -55,6 +57,18 @@
     private static final int TEST_TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC = 0;
     private static final int TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC = 5;
 
+    @BeforeClass
+    public static void presuite() {
+        TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+        testUtil.disableDeviceConfigSyncForTests(true);
+    }
+
+    @AfterClass
+    public static void postsuite() {
+        TestUtil testUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+        testUtil.disableDeviceConfigSyncForTests(false);
+    }
+
     @Before
     public void setup() throws Exception {
         mTestUtil.overrideKillSwitches(true);
@@ -81,6 +95,7 @@
 
     @After
     public void teardown() {
+        mTestUtil.disableDeviceConfigSyncForTests(false);
         mTestUtil.overrideKillSwitches(false);
         mTestUtil.overrideEpochPeriod(TOPICS_EPOCH_JOB_PERIOD_MS);
         mTestUtil.overridePercentageForRandomTopic(TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC);
@@ -99,15 +114,15 @@
         // Skip the test if the right SDK extension is not present.
         Assume.assumeTrue(
                 VersionCompatUtil.INSTANCE.isTestableVersion(
-                        /* minAdServicesVersion=*/ 4,
-                        /* minExtServicesVersion=*/ 9));
+                        /* minAdServicesVersion= */ 4, /* minExtServicesVersion= */ 9));
 
         TopicsManagerFutures topicsManager =
                 TopicsManagerFutures.from(ApplicationProvider.getApplicationContext());
-        GetTopicsRequest request = new GetTopicsRequest.Builder()
-                .setAdsSdkName("sdk1")
-                .setShouldRecordObservation(true)
-                .build();
+        GetTopicsRequest request =
+                new GetTopicsRequest.Builder()
+                        .setAdsSdkName("sdk1")
+                        .setShouldRecordObservation(true)
+                        .build();
         GetTopicsResponse response = topicsManager.getTopicsAsync(request).get();
 
         // At beginning, Sdk1 receives no topic.
@@ -141,10 +156,11 @@
         assertThat(topic.getTaxonomyVersion()).isAtLeast(1L);
 
         // Sdk 2 did not call getTopics API. So it should not receive any topic.
-        GetTopicsResponse response2 = topicsManager.getTopicsAsync(
-                new GetTopicsRequest.Builder()
-                        .setAdsSdkName("sdk2")
-                        .build()).get();
+        GetTopicsResponse response2 =
+                topicsManager
+                        .getTopicsAsync(
+                                new GetTopicsRequest.Builder().setAdsSdkName("sdk2").build())
+                        .get();
         assertThat(response2.getTopics()).isEmpty();
     }
 }
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
index efe5576..5986858 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.privacysandbox.ads.adservices.adid
 
+import android.adservices.common.AdServicesOutcomeReceiver
 import android.content.Context
 import android.os.OutcomeReceiver
 import android.os.ext.SdkExtensions
@@ -51,13 +52,14 @@
 class AdIdManagerTest {
     private var mSession: StaticMockitoSession? = null
     private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
-    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+    private val mValidAdExtServicesSdkExtVersionS = AdServicesInfo.extServicesVersionS() >= 9
+    private val mValidAdExtServicesSdkExtVersionR = AdServicesInfo.extServicesVersionR() >= 11
 
     @Before
     fun setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext<Context>())
 
-        if (mValidAdExtServicesSdkExtVersion) {
+        if (mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR) {
             // setup a mockitoSession to return the mocked manager
             // when the static method .get() is called
             mSession = ExtendedMockito.mockitoSession()
@@ -75,13 +77,15 @@
     @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
     fun testAdIdOlderVersions() {
         Assume.assumeTrue("maxSdkVersion = API 33 ext 3", !mValidAdServicesSdkExtVersion)
-        Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersion)
+        Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersionS)
+        Assume.assumeTrue("maxSdkVersion = API 30 ext 10", !mValidAdExtServicesSdkExtVersionR)
         assertThat(AdIdManager.obtain(mContext)).isNull()
     }
 
     @Test
     fun testAdIdManagerNoClassDefFoundError() {
-        Assume.assumeTrue("minSdkVersion = API 31/32 ext 9", mValidAdExtServicesSdkExtVersion)
+        Assume.assumeTrue("minSdkVersion = API 31/32 ext 9 or API 30 ext 11",
+            mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR)
 
         `when`(android.adservices.adid.AdIdManager.get(any())).thenThrow(NoClassDefFoundError())
         assertThat(AdIdManager.obtain(mContext)).isNull()
@@ -89,11 +93,18 @@
 
     @Test
     fun testAdIdAsync() {
-        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
-            mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersion)
+        val validExtServicesVersion =
+            mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4 or API 31/32 ext 9 or API 30 ext 11",
+            mValidAdServicesSdkExtVersion || validExtServicesVersion)
 
-        val adIdManager = mockAdIdManager(mContext, mValidAdExtServicesSdkExtVersion)
-        setupResponse(adIdManager)
+        val adIdManager = mockAdIdManager(mContext, validExtServicesVersion)
+
+        when (mValidAdExtServicesSdkExtVersionR) {
+            true -> setupResponseR(adIdManager)
+            false -> setupResponseSPlus(adIdManager)
+        }
+
         val managerCompat = AdIdManager.obtain(mContext)
 
         // Actually invoke the compat code.
@@ -102,10 +113,10 @@
         }
 
         // Verify that the compat code was invoked correctly.
-        verify(adIdManager).getAdId(
-            any<Executor>(),
-            any<OutcomeReceiver<android.adservices.adid.AdId, java.lang.Exception>>()
-        )
+        when (mValidAdExtServicesSdkExtVersionR) {
+            true -> verifyOnR(adIdManager)
+            false -> verifyOnSPlus(adIdManager)
+        }
 
         // Verify that the result of the compat call is correct.
         verifyResponse(result)
@@ -132,7 +143,7 @@
             return adIdManager
         }
 
-        private fun setupResponse(adIdManager: android.adservices.adid.AdIdManager) {
+        private fun setupResponseSPlus(adIdManager: android.adservices.adid.AdIdManager) {
             // Set up the response that AdIdManager will return when the compat code calls it.
             val adId = android.adservices.adid.AdId("1234", false)
             val answer = { args: InvocationOnMock ->
@@ -144,10 +155,39 @@
             doAnswer(answer)
                 .`when`(adIdManager).getAdId(
                     any<Executor>(),
-                    any<OutcomeReceiver<android.adservices.adid.AdId, java.lang.Exception>>()
+                    any<OutcomeReceiver<android.adservices.adid.AdId, Exception>>()
                 )
         }
 
+        private fun setupResponseR(adIdManager: android.adservices.adid.AdIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val adId = android.adservices.adid.AdId("1234", false)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>(1)
+                receiver.onResult(adId)
+                null
+            }
+            doAnswer(answer).`when`(adIdManager).getAdId(
+                    any<Executor>(),
+                    any<AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>()
+                )
+        }
+
+        private fun verifyOnR(adIdManager: android.adservices.adid.AdIdManager) {
+            verify(adIdManager).getAdId(
+                any<Executor>(),
+                any<AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>()
+            )
+        }
+
+        private fun verifyOnSPlus(adIdManager: android.adservices.adid.AdIdManager) {
+            verify(adIdManager).getAdId(
+                any<Executor>(),
+                any<OutcomeReceiver<android.adservices.adid.AdId, Exception>>()
+            )
+        }
+
         private fun verifyResponse(adId: AdId) {
             Assert.assertEquals("1234", adId.adId)
             Assert.assertEquals(false, adId.isLimitAdTrackingEnabled)
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
index 534818d..66f88dd 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
@@ -60,7 +60,7 @@
 class AdSelectionManagerTest {
     private var mSession: StaticMockitoSession? = null
     private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
-    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersionS() >= 9
 
     @Before
     fun setUp() {
@@ -164,7 +164,7 @@
 
         /* API is not available */
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
-            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersionS() < 10)
 
         mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
@@ -186,7 +186,7 @@
 
         /* API is not available */
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
-            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersionS() < 10)
 
         mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
@@ -212,7 +212,7 @@
 
         /* API is not available */
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
-            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersionS() < 10)
 
         mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
@@ -236,7 +236,7 @@
 
         /* API is not available */
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
-            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersionS() < 10)
 
         mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
@@ -277,7 +277,7 @@
     @Test
     fun testSelectAdsFromOutcomes() {
         Assume.assumeTrue("minSdkVersion = API 31 ext 10",
-            AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersion() >= 10)
+            AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersionS() >= 10)
 
         val adSelectionManager = mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupAdSelectionFromOutcomesResponse(adSelectionManager)
@@ -389,7 +389,7 @@
     fun testPersistAdSelectionResult() {
         Assume.assumeTrue("minSdkVersion = API 31 ext 10",
             AdServicesInfo.adServicesVersion() >= 10 ||
-                AdServicesInfo.extServicesVersion() >= 10)
+                AdServicesInfo.extServicesVersionS() >= 10)
 
         val adSelectionManager = mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         setupGetAdSelectionResponse(adSelectionManager)
@@ -667,7 +667,7 @@
             request: android.adservices.adselection.ReportEventRequest
         ) {
             val checkInputEvent = AdServicesInfo.adServicesVersion() >= 10 ||
-                AdServicesInfo.extServicesVersion() >= 10
+                AdServicesInfo.extServicesVersionS() >= 10
             val expectedRequestBuilder = android.adservices.adselection.ReportEventRequest.Builder(
                 adSelectionId,
                 eventKey,
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
index 2e4aeb3..02e3377 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
@@ -50,7 +50,7 @@
 class AppSetIdManagerTest {
     private var mSession: StaticMockitoSession? = null
     private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
-    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersionS() >= 9
 
     @Before
     fun setUp() {
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
index e26f9176..9359df6 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
@@ -60,7 +60,7 @@
 
     private var mSession: StaticMockitoSession? = null
     private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
-    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersionS() >= 9
 
     @Before
     fun setUp() {
@@ -106,7 +106,7 @@
 
         /* API is not available */
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
-            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
+            AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersionS() < 10)
         mockCustomAudienceManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
 
@@ -161,7 +161,7 @@
     fun testFetchAndJoinCustomAudience() {
         Assume.assumeTrue("minSdkVersion = API 31 ext 10",
             AdServicesInfo.adServicesVersion() >= 10 ||
-                AdServicesInfo.extServicesVersion() >= 10)
+                AdServicesInfo.extServicesVersionS() >= 10)
 
         val customAudienceManager =
             mockCustomAudienceManager(mContext, mValidAdExtServicesSdkExtVersion)
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
index a88adb8..0543834 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
@@ -61,7 +61,7 @@
 
     private var mSession: StaticMockitoSession? = null
     private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 5
-    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersionS() >= 9
 
     @Before
     fun setUp() {
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
index 704c406..b7c0324 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
@@ -55,7 +55,7 @@
     private var mSession: StaticMockitoSession? = null
     private val mValidAdServicesSdkExt4Version = AdServicesInfo.adServicesVersion() >= 4
     private val mValidAdServicesSdkExt5Version = AdServicesInfo.adServicesVersion() >= 5
-    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersion() >= 9
+    private val mValidAdExtServicesSdkExtVersion = AdServicesInfo.extServicesVersionS() >= 9
 
     @Before
     fun setUp() {
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
index ffb30192..450eecf 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
@@ -53,10 +53,14 @@
         fun obtain(context: Context): AdIdManager? {
             return if (AdServicesInfo.adServicesVersion() >= 4) {
                 AdIdManagerApi33Ext4Impl(context)
-            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+            } else if (AdServicesInfo.extServicesVersionS() >= 9) {
                 BackCompatManager.getManager(context, "AdIdManager") {
                     AdIdManagerApi31Ext9Impl(context)
                 }
+            } else if (AdServicesInfo.extServicesVersionR() >= 11) {
+                BackCompatManager.getManager(context, "AdIdManager") {
+                    AdIdManagerApi30Ext11Impl(context)
+                }
             } else {
                 null
             }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi30Ext11Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi30Ext11Impl.kt
new file mode 100644
index 0000000..e8852b0
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi30Ext11Impl.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.adid
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import androidx.privacysandbox.ads.adservices.internal.asAdServicesOutcomeReceiver
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("ClassVerificationFailure", "NewApi")
+@RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
+open class AdIdManagerApi30Ext11Impl(context: Context) : AdIdManager() {
+    private val mAdIdManager: android.adservices.adid.AdIdManager =
+        android.adservices.adid.AdIdManager.get(context)
+
+    @DoNotInline
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+    override suspend fun getAdId(): AdId {
+        return convertResponse(getAdIdAsyncInternal())
+    }
+
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+    private suspend fun
+        getAdIdAsyncInternal(): android.adservices.adid.AdId = suspendCancellableCoroutine {
+            continuation ->
+        mAdIdManager.getAdId(
+            Runnable::run,
+            continuation.asAdServicesOutcomeReceiver()
+        )
+    }
+
+    private fun convertResponse(response: android.adservices.adid.AdId): AdId {
+        return AdId(response.adId, response.isLimitAdTrackingEnabled)
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
index 85119c2..1b08e51 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
@@ -289,7 +289,7 @@
         fun obtain(context: Context): AdSelectionManager? {
             return if (AdServicesInfo.adServicesVersion() >= 4) {
                 AdSelectionManagerApi33Ext4Impl(context)
-            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+            } else if (AdServicesInfo.extServicesVersionS() >= 9) {
                 BackCompatManager.getManager(context, "AdSelectionManager") {
                     AdSelectionManagerApi31Ext9Impl(context)
                 }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt
index 7b9ad2d..704b9564 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt
@@ -64,7 +64,8 @@
     @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
     override suspend fun selectAds(adSelectionFromOutcomesConfig: AdSelectionFromOutcomesConfig):
         AdSelectionOutcome {
-        if (AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersion() >= 10) {
+        if (AdServicesInfo.adServicesVersion() >= 10 ||
+            AdServicesInfo.extServicesVersionS() >= 10) {
             return Ext10Impl.selectAds(
                 mAdSelectionManager,
                 adSelectionFromOutcomesConfig
@@ -88,7 +89,7 @@
     @DoNotInline
     @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
     override suspend fun reportEvent(reportEventRequest: ReportEventRequest) {
-        if (AdServicesInfo.adServicesVersion() >= 8 || AdServicesInfo.extServicesVersion() >= 9) {
+        if (AdServicesInfo.adServicesVersion() >= 8 || AdServicesInfo.extServicesVersionS() >= 9) {
             return Ext8Impl.reportEvent(
                 mAdSelectionManager,
                 reportEventRequest
@@ -103,7 +104,7 @@
     override suspend fun updateAdCounterHistogram(
         updateAdCounterHistogramRequest: UpdateAdCounterHistogramRequest
     ) {
-        if (AdServicesInfo.adServicesVersion() >= 8 || AdServicesInfo.extServicesVersion() >= 9) {
+        if (AdServicesInfo.adServicesVersion() >= 8 || AdServicesInfo.extServicesVersionS() >= 9) {
             return Ext8Impl.updateAdCounterHistogram(
                 mAdSelectionManager,
                 updateAdCounterHistogramRequest
@@ -118,7 +119,8 @@
     override suspend fun getAdSelectionData(
         getAdSelectionDataRequest: GetAdSelectionDataRequest
     ): GetAdSelectionDataOutcome {
-        if (AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersion() >= 10) {
+        if (AdServicesInfo.adServicesVersion() >= 10 ||
+            AdServicesInfo.extServicesVersionS() >= 10) {
             return Ext10Impl.getAdSelectionData(
                 mAdSelectionManager,
                 getAdSelectionDataRequest
@@ -132,7 +134,8 @@
     override suspend fun persistAdSelectionResult(
         persistAdSelectionResultRequest: PersistAdSelectionResultRequest
     ): AdSelectionOutcome {
-        if (AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersion() >= 10) {
+        if (AdServicesInfo.adServicesVersion() >= 10 ||
+            AdServicesInfo.extServicesVersionS() >= 10) {
             return Ext10Impl.persistAdSelectionResult(
                 mAdSelectionManager,
                 persistAdSelectionResultRequest
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportEventRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportEventRequest.kt
index 344be83..43320f6 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportEventRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportEventRequest.kt
@@ -94,7 +94,8 @@
     @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 8)
     @RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
     internal fun convertToAdServices(): android.adservices.adselection.ReportEventRequest {
-        if (AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersion() >= 10) {
+        if (AdServicesInfo.adServicesVersion() >= 10 ||
+            AdServicesInfo.extServicesVersionS() >= 10) {
             return Ext10Impl.convertReportEventRequest(this)
         }
         return Ext8Impl.convertReportEventRequest(this)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
index f8c9e92..0efa6c3 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
@@ -68,7 +68,8 @@
     @SuppressLint("NewApi")
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     internal fun convertToAdServices(): android.adservices.adselection.ReportImpressionRequest {
-        if (AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersion() >= 10) {
+        if (AdServicesInfo.adServicesVersion() >= 10 ||
+            AdServicesInfo.extServicesVersionS() >= 10) {
             return Ext10Impl.convertReportImpressionRequest(this)
         }
         return Ext4Impl.convertReportImpressionRequest(this)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
index 8a7ad97..334d772 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
@@ -48,7 +48,7 @@
         fun obtain(context: Context): AppSetIdManager? {
             return if (AdServicesInfo.adServicesVersion() >= 4) {
                 AppSetIdManagerApi33Ext4Impl(context)
-            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+            } else if (AdServicesInfo.extServicesVersionS() >= 9) {
                 BackCompatManager.getManager(context, "AppSetIdManager") {
                     AppSetIdManagerApi31Ext9Impl(context)
                 }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
index b1db375..8c89de6 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
@@ -102,10 +102,11 @@
     @SuppressLint("NewApi")
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     internal fun convertToAdServices(): android.adservices.common.AdData {
-        if (AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersion() >= 10) {
+        if (AdServicesInfo.adServicesVersion() >= 10 ||
+            AdServicesInfo.extServicesVersionS() >= 10) {
             return Ext10Impl.convertAdData(this)
         } else if (AdServicesInfo.adServicesVersion() >= 8 ||
-                AdServicesInfo.extServicesVersion() >= 9) {
+                AdServicesInfo.extServicesVersionS() >= 9) {
             return Ext8Impl.convertAdData(this)
         }
         return Ext4Impl.convertAdData(this)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
index b065279..9127b1d 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
@@ -138,7 +138,7 @@
         fun obtain(context: Context): CustomAudienceManager? {
             return if (AdServicesInfo.adServicesVersion() >= 4) {
                 CustomAudienceManagerApi33Ext4Impl(context)
-            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+            } else if (AdServicesInfo.extServicesVersionS() >= 9) {
                 BackCompatManager.getManager(context, "CustomAudienceManager") {
                     CustomAudienceManagerApi31Ext9Impl(context)
                 }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt
index 031635b..623056c 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt
@@ -53,7 +53,8 @@
     @DoNotInline
     @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
     override suspend fun fetchAndJoinCustomAudience(request: FetchAndJoinCustomAudienceRequest) {
-        if (AdServicesInfo.adServicesVersion() >= 10 || AdServicesInfo.extServicesVersion() >= 10) {
+        if (AdServicesInfo.adServicesVersion() >= 10 ||
+            AdServicesInfo.extServicesVersionS() >= 10) {
             return Ext10Impl.fetchAndJoinCustomAudience(customAudienceManager, request)
         }
         throw UnsupportedOperationException("API is not available. Min version is API 31 ext 10")
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
index 535e485..a5eee78 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
@@ -30,9 +30,17 @@
         }
     }
 
-    fun extServicesVersion(): Int {
+    fun extServicesVersionS(): Int {
         return if (Build.VERSION.SDK_INT == 31 || Build.VERSION.SDK_INT == 32) {
-            Extensions30ExtImpl.getAdExtServicesVersion()
+            Extensions30ExtImpl.getAdExtServicesVersionS()
+        } else {
+            0
+        }
+    }
+
+    fun extServicesVersionR(): Int {
+        return if (Build.VERSION.SDK_INT == 30) {
+            Extensions30ExtImpl.getAdExtServicesVersionR()
         } else {
             0
         }
@@ -51,7 +59,11 @@
         // for the build version. Use S for now, but this can be changed to R when we add
         // support for R later.
         @DoNotInline
-        fun getAdExtServicesVersion() =
+        fun getAdExtServicesVersionS() =
             SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S)
+
+        @DoNotInline
+        fun getAdExtServicesVersionR() =
+            SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R)
     }
 }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesOutcomeReceiver.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesOutcomeReceiver.kt
new file mode 100644
index 0000000..450650d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesOutcomeReceiver.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.internal
+
+import android.adservices.common.AdServicesOutcomeReceiver
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.annotation.RequiresExtension
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+/*
+  This file is a modified version OutcomeReceiver.kt in androidx.core.os, designed to provide the same
+  functionality with the AdServicesOutcomeReceiver, to keep the implementation of the backward compatible
+  classes as close to identical as possible.
+ */
+
+@RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
+fun <R, E : Throwable> Continuation<R>.asAdServicesOutcomeReceiver():
+    AdServicesOutcomeReceiver<R, E> = ContinuationOutcomeReceiver(this)
+
+@SuppressLint("NewApi")
+@RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
+private class ContinuationOutcomeReceiver<R, E : Throwable>(
+    private val continuation: Continuation<R>
+) : AdServicesOutcomeReceiver<R, E>, AtomicBoolean(false) {
+    @Suppress("WRONG_NULLABILITY_FOR_JAVA_OVERRIDE")
+    override fun onResult(result: R) {
+        // Do not attempt to resume more than once, even if the caller of the returned
+        // OutcomeReceiver is buggy and tries anyway.
+        if (compareAndSet(false, true)) {
+            continuation.resume(result)
+        }
+    }
+
+    override fun onError(error: E) {
+        // Do not attempt to resume more than once, even if the caller of the returned
+        // OutcomeReceiver is buggy and tries anyway.
+        if (compareAndSet(false, true)) {
+            continuation.resumeWithException(error)
+        }
+    }
+
+    override fun toString() = "ContinuationOutcomeReceiver(outcomeReceived = ${get()})"
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/BackCompatManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/BackCompatManager.kt
index ff38355..f0f6005 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/BackCompatManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/BackCompatManager.kt
@@ -29,7 +29,8 @@
             Log.d(
                 tag,
                 "Unable to find adservices code, check manifest for uses-library tag, " +
-                    "version=${AdServicesInfo.extServicesVersion()}"
+                    "versionR=${AdServicesInfo.extServicesVersionR()}, " +
+                    "versionS=${AdServicesInfo.extServicesVersionS()}"
             )
             return null
         }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
index 9ea4e88..846acbb 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
@@ -153,7 +153,7 @@
                 "AdServicesInfo.version=${AdServicesInfo.adServicesVersion()}")
             return if (AdServicesInfo.adServicesVersion() >= 5) {
                 MeasurementManagerApi33Ext5Impl(context)
-            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+            } else if (AdServicesInfo.extServicesVersionS() >= 9) {
                 BackCompatManager.getManager(context, "MeasurementManager") {
                     MeasurementManagerApi31Ext9Impl(context)
                 }
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
index 70cd4f0..97ec4ba 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
@@ -55,7 +55,7 @@
                 TopicsManagerApi33Ext5Impl(context)
             } else if (AdServicesInfo.adServicesVersion() == 4) {
                 TopicsManagerApi33Ext4Impl(context)
-            } else if (AdServicesInfo.extServicesVersion() >= 9) {
+            } else if (AdServicesInfo.extServicesVersionS() >= 9) {
                 BackCompatManager.getManager(context, "TopicsManager") {
                     TopicsManagerApi31Ext9Impl(context)
                 }
diff --git a/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigatorDestinationBuilder.kt b/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigatorDestinationBuilder.kt
index f590746..d321417 100644
--- a/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigatorDestinationBuilder.kt
+++ b/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigatorDestinationBuilder.kt
@@ -18,7 +18,6 @@
 
 package androidx.testutils
 
-import android.annotation.SuppressLint
 import androidx.annotation.IdRes
 import androidx.navigation.ExperimentalSafeArgsApi
 import androidx.navigation.NavDestinationBuilder
@@ -85,6 +84,5 @@
     constructor(navigator: TestNavigator, @IdRes id: Int = 0) : super(navigator, id)
     constructor(navigator: TestNavigator, route: String) : super(navigator, route)
     @OptIn(ExperimentalSafeArgsApi::class)
-    @SuppressLint("NullAnnotationGroup")
     constructor(navigator: TestNavigator, route: KClass<*>) : super(navigator, route, emptyMap())
 }
diff --git a/transition/transition/build.gradle b/transition/transition/build.gradle
index dce4a5e..00b1295 100644
--- a/transition/transition/build.gradle
+++ b/transition/transition/build.gradle
@@ -15,7 +15,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
-    api(project(":core:core"))
+    api("androidx.core:core:1.13.0")
     implementation("androidx.collection:collection:1.1.0")
     compileOnly(projectOrArtifact(":fragment:fragment"))
     compileOnly("androidx.appcompat:appcompat:1.0.1")
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 0c45b56..909fe29 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -493,18 +493,18 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Border getFocusedDisabledBorder();
     method public float getIconSize();
     method public float getIconSizeDense();
-    method public float getListItemElevation();
     method public androidx.compose.foundation.shape.RoundedCornerShape getListItemShape();
+    method public float getTonalElevation();
     method public androidx.tv.material3.ListItemGlow 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.ListItemScale 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.ListItemShape 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);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ListItemShape 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 @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Border FocusedDisabledBorder;
     property public final float IconSize;
     property public final float IconSizeDense;
-    property public final float ListItemElevation;
     property public final androidx.compose.foundation.shape.RoundedCornerShape ListItemShape;
+    property public final float TonalElevation;
     field public static final androidx.tv.material3.ListItemDefaults INSTANCE;
-    field public static final float SelectedContinerColorOpacity = 0.4f;
+    field public static final float SelectedContainerColorOpacity = 0.4f;
   }
 
   @androidx.compose.runtime.Immutable public final class ListItemGlow {
@@ -524,8 +524,8 @@
   }
 
   public final class ListItemKt {
-    method @androidx.compose.runtime.Composable public static void DenseListItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.ListItemShape shape, optional androidx.tv.material3.ListItemColors colors, optional androidx.tv.material3.ListItemScale scale, optional androidx.tv.material3.ListItemBorder border, optional androidx.tv.material3.ListItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> headlineContent);
-    method @androidx.compose.runtime.Composable public static void ListItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.ListItemShape shape, optional androidx.tv.material3.ListItemColors colors, optional androidx.tv.material3.ListItemScale scale, optional androidx.tv.material3.ListItemBorder border, optional androidx.tv.material3.ListItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> headlineContent);
+    method @androidx.compose.runtime.Composable public static void DenseListItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> headlineContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.ListItemShape shape, optional androidx.tv.material3.ListItemColors colors, optional androidx.tv.material3.ListItemScale scale, optional androidx.tv.material3.ListItemBorder border, optional androidx.tv.material3.ListItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+    method @androidx.compose.runtime.Composable public static void ListItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> headlineContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.ListItemShape shape, optional androidx.tv.material3.ListItemColors colors, optional androidx.tv.material3.ListItemScale scale, optional androidx.tv.material3.ListItemBorder border, optional androidx.tv.material3.ListItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
   @androidx.compose.runtime.Immutable public final class ListItemScale {
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 0c45b56..909fe29 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -493,18 +493,18 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Border getFocusedDisabledBorder();
     method public float getIconSize();
     method public float getIconSizeDense();
-    method public float getListItemElevation();
     method public androidx.compose.foundation.shape.RoundedCornerShape getListItemShape();
+    method public float getTonalElevation();
     method public androidx.tv.material3.ListItemGlow 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.ListItemScale 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.ListItemShape 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);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ListItemShape 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 @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Border FocusedDisabledBorder;
     property public final float IconSize;
     property public final float IconSizeDense;
-    property public final float ListItemElevation;
     property public final androidx.compose.foundation.shape.RoundedCornerShape ListItemShape;
+    property public final float TonalElevation;
     field public static final androidx.tv.material3.ListItemDefaults INSTANCE;
-    field public static final float SelectedContinerColorOpacity = 0.4f;
+    field public static final float SelectedContainerColorOpacity = 0.4f;
   }
 
   @androidx.compose.runtime.Immutable public final class ListItemGlow {
@@ -524,8 +524,8 @@
   }
 
   public final class ListItemKt {
-    method @androidx.compose.runtime.Composable public static void DenseListItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.ListItemShape shape, optional androidx.tv.material3.ListItemColors colors, optional androidx.tv.material3.ListItemScale scale, optional androidx.tv.material3.ListItemBorder border, optional androidx.tv.material3.ListItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> headlineContent);
-    method @androidx.compose.runtime.Composable public static void ListItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.ListItemShape shape, optional androidx.tv.material3.ListItemColors colors, optional androidx.tv.material3.ListItemScale scale, optional androidx.tv.material3.ListItemBorder border, optional androidx.tv.material3.ListItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> headlineContent);
+    method @androidx.compose.runtime.Composable public static void DenseListItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> headlineContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.ListItemShape shape, optional androidx.tv.material3.ListItemColors colors, optional androidx.tv.material3.ListItemScale scale, optional androidx.tv.material3.ListItemBorder border, optional androidx.tv.material3.ListItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+    method @androidx.compose.runtime.Composable public static void ListItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> headlineContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.ListItemShape shape, optional androidx.tv.material3.ListItemColors colors, optional androidx.tv.material3.ListItemScale scale, optional androidx.tv.material3.ListItemBorder border, optional androidx.tv.material3.ListItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
   @androidx.compose.runtime.Immutable public final class ListItemScale {
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ListItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ListItem.kt
index 55e74b6..0bc961f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ListItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ListItem.kt
@@ -49,6 +49,7 @@
  *
  * @param selected defines whether this ListItem is selected or not
  * @param onClick called when this ListItem is clicked
+ * @param headlineContent the [Composable] headline content of the list item
  * @param modifier [Modifier] to be applied to the list item
  * @param enabled controls the enabled state of this list item. When `false`, this component will
  * not respond to user input, and it will appear visually disabled and disabled to accessibility
@@ -73,12 +74,12 @@
  * emitting [Interaction]s for this list item. You can use this to change the list item's appearance
  * or preview the list item in different states. Note that if `null` is provided, interactions will
  * still happen internally.
- * @param headlineContent the [Composable] headline content of the list item
  */
 @Composable
 fun ListItem(
     selected: Boolean,
     onClick: () -> Unit,
+    headlineContent: @Composable () -> Unit,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     onLongClick: (() -> Unit)? = null,
@@ -86,14 +87,13 @@
     supportingContent: (@Composable () -> Unit)? = null,
     leadingContent: (@Composable BoxScope.() -> Unit)? = null,
     trailingContent: (@Composable () -> Unit)? = null,
-    tonalElevation: Dp = ListItemDefaults.ListItemElevation,
+    tonalElevation: Dp = ListItemDefaults.TonalElevation,
     shape: ListItemShape = ListItemDefaults.shape(),
     colors: ListItemColors = ListItemDefaults.colors(),
     scale: ListItemScale = ListItemDefaults.scale(),
     border: ListItemBorder = ListItemDefaults.border(),
     glow: ListItemGlow = ListItemDefaults.glow(),
-    interactionSource: MutableInteractionSource? = null,
-    headlineContent: @Composable () -> Unit
+    interactionSource: MutableInteractionSource? = null
 ) {
     BaseListItem(
         selected = selected,
@@ -140,6 +140,7 @@
  *
  * @param selected defines whether this ListItem is selected or not
  * @param onClick called when this ListItem is clicked
+ * @param headlineContent the [Composable] headline content of the list item
  * @param modifier [Modifier] to be applied to the list item
  * @param enabled controls the enabled state of this list item. When `false`, this component will
  * not respond to user input, and it will appear visually disabled and disabled to accessibility
@@ -164,12 +165,12 @@
  * emitting [Interaction]s for this list item. You can use this to change the list item's appearance
  * or preview the list item in different states. Note that if `null` is provided, interactions will
  * still happen internally.
- * @param headlineContent the [Composable] headline content of the list item
  */
 @Composable
 fun DenseListItem(
     selected: Boolean,
     onClick: () -> Unit,
+    headlineContent: @Composable () -> Unit,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     onLongClick: (() -> Unit)? = null,
@@ -177,14 +178,13 @@
     supportingContent: (@Composable () -> Unit)? = null,
     leadingContent: (@Composable BoxScope.() -> Unit)? = null,
     trailingContent: (@Composable () -> Unit)? = null,
-    tonalElevation: Dp = ListItemDefaults.ListItemElevation,
+    tonalElevation: Dp = ListItemDefaults.TonalElevation,
     shape: ListItemShape = ListItemDefaults.shape(),
     colors: ListItemColors = ListItemDefaults.colors(),
     scale: ListItemScale = ListItemDefaults.scale(),
     border: ListItemBorder = ListItemDefaults.border(),
     glow: ListItemGlow = ListItemDefaults.glow(),
-    interactionSource: MutableInteractionSource? = null,
-    headlineContent: @Composable () -> Unit
+    interactionSource: MutableInteractionSource? = null
 ) {
     BaseListItem(
         selected = selected,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ListItemDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ListItemDefaults.kt
index 982c1ce..1ea5747 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ListItemDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ListItemDefaults.kt
@@ -45,7 +45,7 @@
     /**
      * The default elevation used by [ListItem].
      */
-    val ListItemElevation = Elevation.Level0
+    val TonalElevation = Elevation.Level0
 
     /**
      * The default shape for a [ListItem].
@@ -67,7 +67,7 @@
     /**
      * The default opacity for the [ListItem] container color in selected state.
      */
-    const val SelectedContinerColorOpacity = 0.4f
+    const val SelectedContainerColorOpacity = 0.4f
 
     /**
      * The default content padding [PaddingValues] used by [ListItem]
@@ -116,6 +116,8 @@
      * @param pressedSelectedShape the shape used when the ListItem is enabled, pressed and
      * selected
      */
+    @ReadOnlyComposable
+    @Composable
     fun shape(
         shape: Shape = ListItemShape,
         focusedShape: Shape = shape,
@@ -172,7 +174,7 @@
         pressedContainerColor: Color = focusedContainerColor,
         pressedContentColor: Color = contentColorFor(focusedContainerColor),
         selectedContainerColor: Color = MaterialTheme.colorScheme.secondaryContainer
-            .copy(alpha = SelectedContinerColorOpacity),
+            .copy(alpha = SelectedContainerColorOpacity),
         selectedContentColor: Color = MaterialTheme.colorScheme.onSecondaryContainer,
         disabledContainerColor: Color = Color.Transparent,
         disabledContentColor: Color = MaterialTheme.colorScheme.onSurface,
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxScreenshotTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxScreenshotTest.kt
index 673dc72..4136132 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxScreenshotTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxScreenshotTest.kt
@@ -32,7 +32,6 @@
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -52,7 +51,6 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTestApi::class)
 class SwipeToDismissBoxScreenshotTest {
 
     @get:Rule